diff -Naur server-script.ori/main.js server-script/main.js --- server-script.ori/main.js 2016-02-07 14:27:52.000000000 +0100 +++ server-script/main.js 2016-02-06 20:36:55.331908378 +0100 @@ -1,259 +1,262 @@ -var env = require('env'); -var server_utils = require('server_utils'); -var sim_utils = require('sim_utils'); -var utils = require('utils'); -var content_manager = require('content_manager'); -var _ = require('thirdparty/lodash'); - -// Timeout values, in seconds. -var EMPTY_TIMEOUT = 120; - -function shutdownWhenEmpty() { - var emptyTimeout; - utils.pushCallback(server, 'onConnect', function(onConnect, client, reconnect) { - if (!client.rejected && emptyTimeout) { - clearTimeout(emptyTimeout); - emptyTimeout = undefined; - } - return onConnect; - }); - setInterval(function() { - var before = server.connected; - server_utils.refreshConnectionCount(); - if (server.connected != before) { - console.error('Whoa! Connection count got out of sync! Something went wrong somewhere. (before=', before, ', after=', server.connected, ')'); - } - if (!emptyTimeout && server.connected === 0) { - emptyTimeout = setTimeout(function() { - sim.shutdown(true); - server.exit(); - }, EMPTY_TIMEOUT * 1000); - } - }, 1000); -} - -function setState(newState) { - console.log("Changing state from", curState.name, "to", newState.name); - - if (newState === curState) - return; - - if (curState.exit && !curState.exit(newState)) - { - console.log("Cancelling state change"); - - return; - } - - curState = newState; - server.setGameState(curState.name); - - // Calling both of these APIs is primarily a matter of getting test coverage - // One or the other can be removed as deemed necessary - server.incrementTitleStatistic("State_" + curState.name, 1); - server.recordGameEvent("State_" + curState.name); - - var result; - if (newState.enter) - result = newState.enter.apply(this, Array.prototype.slice.call(arguments).slice(1)); - if (curState !== newState) - return; // This means entering the state caused another state change. - if (result !== undefined) - curState.hello_response.data = result; - else - delete curState.hello_response.data; - var stateMessage = { - message_type: 'server_state', - payload: curState.hello_response - }; - - if (!curState.getClientState) { - server.broadcast(stateMessage); - return; - } - - if (!stateMessage.payload.data) - stateMessage.payload.data = {}; - var numClients = server.clients.length; - for (var c = 0; c < numClients; ++c) { - var client = server.clients[c]; - if (client.connected) { - stateMessage.payload.data.client = curState.getClientState(client); - client.message(stateMessage); - } - } - delete stateMessage.payload.data.client; -} - -function updateStateData(data) -{ - curState.hello_response.data = data; -} - -var states = {}; -var gameModes = {}; - -// Note: Game Modes must be exported before loading the states, since states -// register their game mode as part of loading. -exports.gameModes = gameModes; -exports.gameMode = ''; - -function addState(state, name) { - if (states.hasOwnProperty(name)) - return state[name]; - state.name = name; - state.hello_response = { - state: name, - url: state.url - }; - states[name] = state; - return state; -} - -var cmdlineAllowCheats = (env.indexOf('--allow-cheats') >= 0); -exports.cheats = { - cheat_flags: { - allow_change_vision: cmdlineAllowCheats, - allow_change_control: cmdlineAllowCheats, - allow_create_unit: cmdlineAllowCheats, - allow_mod_data_updates: cmdlineAllowCheats, - - any_enabled: cmdlineAllowCheats - } -} - -function loadState(name) { - if (states.hasOwnProperty(name)) - return states[name]; - var state = require('states/' + name); - if (state) - addState(state, name); - return state; -} - -_.forEach([ - 'empty', - 'config', - 'lobby', - 'landing', - 'playing', - 'game_over', - 'gw_lobby', - 'load_replay', - 'load_save', - 'replay', - 'ladder_lobby' - ], - loadState -); - -var curState = states.empty; - -server.handlers.hello = function(msg) { - var helloMessage = curState.hello_response; - var perClient = !!curState.getClientState; - if (perClient) { - helloMessage.data.client = curState.getClientState(msg.client); - } - server.respond(msg).succeed(helloMessage); - if (perClient) - delete helloMessage.data.client; -}; - -server_utils.debug_messages = env.indexOf('--squelch-messages') < 0; -server_utils.log_lobby_description = env.indexOf('--squelch-messages') < 0; -exports.keep_alive = env.indexOf('--keep-alive') >= 0; -var timeLimit = env.indexOf('--time-limit'); -if (timeLimit >= 0) { - exports.time_limit = Number(env[timeLimit + 1]); - if (isNaN(exports.time_limit) || (exports.time_limit <= 0)) - delete exports.time_limit; - else - console.log("Time limit set to", exports.time_limit, "seconds."); -} -exports.no_players = env.indexOf('--no-players') >= 0; -var spectators = env.indexOf('--spectators'); -if (spectators >= 0) - exports.spectators = Number(env[spectators + 1]); -else - exports.spectators = 0; - -var gameModeIndex = env.indexOf('--game-mode'); -if (gameModeIndex >= 0) -{ - var gameMode = env[gameModeIndex + 1]; - - // Game mode can contain a content specification, in the form of - // "content1,content2:gamemode". This is so that we can pass in - // content requirements through UberNet. - var contentSeparator = gameMode.indexOf(':'); - if (contentSeparator >= 0) - { - var content = gameMode.substr(0, contentSeparator).split(","); - if (!_.isEmpty(content)) - content_manager.setRequiredContent(content); - gameMode = gameMode.substr(contentSeparator + 1); - } - exports.gameMode = gameMode; -} - -exports.setState = setState; -exports.updateStateData = updateStateData; -exports.states = states; -exports.loadState = loadState; -exports.shutdownWhenEmpty = shutdownWhenEmpty; -exports.setStateUrl = function(state, url) { - state.hello_response.url = url; -}; - -var serverNameIndex = env.indexOf('--server-name'); -if (serverNameIndex >= 0) - exports.serverName = env[serverNameIndex + 1]; - -var stateIndex = env.indexOf('--state'); -if (stateIndex >= 0) { - var initialStateName = env[stateIndex + 1]; - var initialState = loadState(initialStateName); - if (initialState) - curState = initialState; - else - console.error("Unable to load state", initialStateName); -} - -var initialStateEnterData = undefined; -var loadReplayIndex = env.indexOf('--load-replay'); -var loadTimeIndex = env.indexOf('--load-time'); /* todo: specify load time with message instead of parameter */ -if (loadReplayIndex >= 0) { - - var path = env[loadReplayIndex + 1]; - var split_index = path.lastIndexOf('#t='); - var replayFileName = (split_index === -1) ? path : path.substr(0, split_index); - var loadTime = loadTimeIndex >= 0 ? env[loadTimeIndex + 1] : -1; - if (split_index !== -1) - loadTime = Number(path.substr(split_index+3)); - if (_.isNaN(loadTime)) - loadTime = -1; - - loadTime = -1; /* server crashes if the load time is not -1. */ - - var replaySentinelFileName = ''; - var replaySentinelFileNameIndex = env.indexOf('--load-replay-sentinel'); - if (replaySentinelFileNameIndex >= 0) - replaySentinelFileName = env[replaySentinelFileNameIndex + 1]; - - initialStateEnterData = { - 'name': replayFileName, - 'sentinel_name': replaySentinelFileName, - 'time': loadTime, - 'view_replay': exports.gameMode !== 'loadsave' - }; - - /* this is a little hacky */ - curState = states.load_save; -} - -// Make sure our current state gets initialized. -var enter_result = curState.enter(initialStateEnterData); -if (enter_result) - updateStateData(enter_result); - +var env = require('env'); +var server_utils = require('server_utils'); +var sim_utils = require('sim_utils'); +var utils = require('utils'); +var content_manager = require('content_manager'); +var _ = require('thirdparty/lodash'); + +// Timeout values, in seconds. +var EMPTY_TIMEOUT = 120; + +exports.MAX_PLAYERS = 10; +exports.MAX_SPECTATORS = 3; + +function shutdownWhenEmpty() { + var emptyTimeout; + utils.pushCallback(server, 'onConnect', function(onConnect, client, reconnect) { + if (!client.rejected && emptyTimeout) { + clearTimeout(emptyTimeout); + emptyTimeout = undefined; + } + return onConnect; + }); + setInterval(function() { + var before = server.connected; + server_utils.refreshConnectionCount(); + if (server.connected != before) { + console.error('Whoa! Connection count got out of sync! Something went wrong somewhere. (before=', before, ', after=', server.connected, ')'); + } + if (!emptyTimeout && server.connected === 0) { + emptyTimeout = setTimeout(function() { + sim.shutdown(true); + server.exit(); + }, EMPTY_TIMEOUT * 1000); + } + }, 1000); +} + +function setState(newState) { + console.log("Changing state from", curState.name, "to", newState.name); + + if (newState === curState) + return; + + if (curState.exit && !curState.exit(newState)) + { + console.log("Cancelling state change"); + + return; + } + + curState = newState; + server.setGameState(curState.name); + + // Calling both of these APIs is primarily a matter of getting test coverage + // One or the other can be removed as deemed necessary + server.incrementTitleStatistic("State_" + curState.name, 1); + server.recordGameEvent("State_" + curState.name); + + var result; + if (newState.enter) + result = newState.enter.apply(this, Array.prototype.slice.call(arguments).slice(1)); + if (curState !== newState) + return; // This means entering the state caused another state change. + if (result !== undefined) + curState.hello_response.data = result; + else + delete curState.hello_response.data; + var stateMessage = { + message_type: 'server_state', + payload: curState.hello_response + }; + + if (!curState.getClientState) { + server.broadcast(stateMessage); + return; + } + + if (!stateMessage.payload.data) + stateMessage.payload.data = {}; + var numClients = server.clients.length; + for (var c = 0; c < numClients; ++c) { + var client = server.clients[c]; + if (client.connected) { + stateMessage.payload.data.client = curState.getClientState(client); + client.message(stateMessage); + } + } + delete stateMessage.payload.data.client; +} + +function updateStateData(data) +{ + curState.hello_response.data = data; +} + +var states = {}; +var gameModes = {}; + +// Note: Game Modes must be exported before loading the states, since states +// register their game mode as part of loading. +exports.gameModes = gameModes; +exports.gameMode = ''; + +function addState(state, name) { + if (states.hasOwnProperty(name)) + return state[name]; + state.name = name; + state.hello_response = { + state: name, + url: state.url + }; + states[name] = state; + return state; +} + +var cmdlineAllowCheats = (env.indexOf('--allow-cheats') >= 0); +exports.cheats = { + cheat_flags: { + allow_change_vision: cmdlineAllowCheats, + allow_change_control: cmdlineAllowCheats, + allow_create_unit: cmdlineAllowCheats, + allow_mod_data_updates: cmdlineAllowCheats, + + any_enabled: cmdlineAllowCheats + } +} + +function loadState(name) { + if (states.hasOwnProperty(name)) + return states[name]; + var state = require('states/' + name); + if (state) + addState(state, name); + return state; +} + +_.forEach([ + 'empty', + 'config', + 'lobby', + 'landing', + 'playing', + 'game_over', + 'gw_lobby', + 'load_replay', + 'load_save', + 'replay', + 'ladder_lobby' + ], + loadState +); + +var curState = states.empty; + +server.handlers.hello = function(msg) { + var helloMessage = curState.hello_response; + var perClient = !!curState.getClientState; + if (perClient) { + helloMessage.data.client = curState.getClientState(msg.client); + } + server.respond(msg).succeed(helloMessage); + if (perClient) + delete helloMessage.data.client; +}; + +server_utils.debug_messages = env.indexOf('--squelch-messages') < 0; +server_utils.log_lobby_description = env.indexOf('--squelch-messages') < 0; +exports.keep_alive = env.indexOf('--keep-alive') >= 0; +var timeLimit = env.indexOf('--time-limit'); +if (timeLimit >= 0) { + exports.time_limit = Number(env[timeLimit + 1]); + if (isNaN(exports.time_limit) || (exports.time_limit <= 0)) + delete exports.time_limit; + else + console.log("Time limit set to", exports.time_limit, "seconds."); +} +exports.no_players = env.indexOf('--no-players') >= 0; +var spectators = env.indexOf('--spectators'); +if (spectators >= 0) + exports.spectators = Number(env[spectators + 1]); +else + exports.spectators = 0; + +var gameModeIndex = env.indexOf('--game-mode'); +if (gameModeIndex >= 0) +{ + var gameMode = env[gameModeIndex + 1]; + + // Game mode can contain a content specification, in the form of + // "content1,content2:gamemode". This is so that we can pass in + // content requirements through UberNet. + var contentSeparator = gameMode.indexOf(':'); + if (contentSeparator >= 0) + { + var content = gameMode.substr(0, contentSeparator).split(","); + if (!_.isEmpty(content)) + content_manager.setRequiredContent(content); + gameMode = gameMode.substr(contentSeparator + 1); + } + exports.gameMode = gameMode; +} + +exports.setState = setState; +exports.updateStateData = updateStateData; +exports.states = states; +exports.loadState = loadState; +exports.shutdownWhenEmpty = shutdownWhenEmpty; +exports.setStateUrl = function(state, url) { + state.hello_response.url = url; +}; + +var serverNameIndex = env.indexOf('--server-name'); +if (serverNameIndex >= 0) + exports.serverName = env[serverNameIndex + 1]; + +var stateIndex = env.indexOf('--state'); +if (stateIndex >= 0) { + var initialStateName = env[stateIndex + 1]; + var initialState = loadState(initialStateName); + if (initialState) + curState = initialState; + else + console.error("Unable to load state", initialStateName); +} + +var initialStateEnterData = undefined; +var loadReplayIndex = env.indexOf('--load-replay'); +var loadTimeIndex = env.indexOf('--load-time'); /* todo: specify load time with message instead of parameter */ +if (loadReplayIndex >= 0) { + + var path = env[loadReplayIndex + 1]; + var split_index = path.lastIndexOf('#t='); + var replayFileName = (split_index === -1) ? path : path.substr(0, split_index); + var loadTime = loadTimeIndex >= 0 ? env[loadTimeIndex + 1] : -1; + if (split_index !== -1) + loadTime = Number(path.substr(split_index+3)); + if (_.isNaN(loadTime)) + loadTime = -1; + + loadTime = -1; /* server crashes if the load time is not -1. */ + + var replaySentinelFileName = ''; + var replaySentinelFileNameIndex = env.indexOf('--load-replay-sentinel'); + if (replaySentinelFileNameIndex >= 0) + replaySentinelFileName = env[replaySentinelFileNameIndex + 1]; + + initialStateEnterData = { + 'name': replayFileName, + 'sentinel_name': replaySentinelFileName, + 'time': loadTime, + 'view_replay': exports.gameMode !== 'loadsave' + }; + + /* this is a little hacky */ + curState = states.load_save; +} + +// Make sure our current state gets initialized. +var enter_result = curState.enter(initialStateEnterData); +if (enter_result) + updateStateData(enter_result); + diff -Naur server-script.ori/states/empty.js server-script/states/empty.js --- server-script.ori/states/empty.js 2016-02-07 14:34:26.000000000 +0100 +++ server-script/states/empty.js 2016-02-07 12:45:49.641872614 +0100 @@ -1,84 +1,87 @@ -var main = require('main'); -var utils = require('utils'); -var content_manager = require('content_manager'); -var _ = require('thirdparty/lodash'); - -var cleanup = []; - -var EMPTY_TIMEOUT = 5 * 60; - -exports.url = ''; -exports.enter = function() { - - if (main.no_players) { - main.setState(main.states.config); - return; - } - - var modNames = []; - var mods = server.getMods(); - if (mods !== undefined && mods.mounted_mods !== undefined) { - _.forEach(mods.mounted_mods, function (element) { - modNames.push(element.display_name); - }); - } - - server.maxClients = 1; - if (main.serverName) - server.beacon = { - uuid: server.uuid(), - full: false, - started: false, - players: 0, - creator: null, - max_players: 1, - spectators: 0, - max_spectators: 0, - mode: 'Waiting', - mod_names: modNames, - cheat_config: main.cheats, - player_names: [], - spectator_names: [], - require_password: false, - whitelist: [], - blacklist: [], - tag: '', - game_name: main.serverName, - game: { - name: main.serverName - }, - required_content: content_manager.getRequiredContent(), - bounty_mode: false, - bounty_value: 0.5, - sandbox: false - }; - else - server.beacon = undefined; - - utils.pushCallback(server, 'onConnect', function(onConnect, client, reconnect) { - if (client.rejected) - { - console.log("Rejected connection from misconfigured client, and shutting down."); - server.exit(); - } - else - main.setState(main.gameModes[main.gameMode] || main.states.lobby, client); - return onConnect; - }); - cleanup.push(function() { server.onConnect.pop(); }); - - if (!main.keep_alive) { - var timeout = setTimeout(function() { server.exit(); }, EMPTY_TIMEOUT * 1000); - cleanup.push(function() { clearTimeout(timeout); }); - } -}; - -exports.exit = function(newState) { - _.forEachRight(cleanup, function(c) { c(); }); - cleanup = []; - - if (server.clients.length && !main.keep_alive) - main.shutdownWhenEmpty(); - - return true; -}; +var main = require('main'); +var utils = require('utils'); +var content_manager = require('content_manager'); +var _ = require('thirdparty/lodash'); + +var cleanup = []; + +var MAX_PLAYERS = main.MAX_PLAYERS; +var MAX_SPECTATORS = main.MAX_SPECTATORS; + +var EMPTY_TIMEOUT = 5 * 60; + +exports.url = ''; +exports.enter = function() { + + if (main.no_players) { + main.setState(main.states.config); + return; + } + + var modNames = []; + var mods = server.getMods(); + if (mods !== undefined && mods.mounted_mods !== undefined) { + _.forEach(mods.mounted_mods, function (element) { + modNames.push(element.display_name); + }); + } + + server.maxClients = 1; + if (main.serverName) + server.beacon = { + uuid: server.uuid(), + full: false, + started: false, + players: 0, + creator: null, + max_players: MAX_PLAYERS, + spectators: 0, + max_spectators: MAX_SPECTATORS, + mode: 'Waiting', + mod_names: modNames, + cheat_config: main.cheats, + player_names: [], + spectator_names: [], + require_password: false, + whitelist: [], + blacklist: [], + tag: '', + game_name: main.serverName, + game: { + name: main.serverName + }, + required_content: content_manager.getRequiredContent(), + bounty_mode: false, + bounty_value: 0.5, + sandbox: false + }; + else + server.beacon = undefined; + + utils.pushCallback(server, 'onConnect', function(onConnect, client, reconnect) { + if (client.rejected) + { + console.log("Rejected connection from misconfigured client, and shutting down."); + server.exit(); + } + else + main.setState(main.gameModes[main.gameMode] || main.states.lobby, client); + return onConnect; + }); + cleanup.push(function() { server.onConnect.pop(); }); + + if (!main.keep_alive) { + var timeout = setTimeout(function() { server.exit(); }, EMPTY_TIMEOUT * 1000); + cleanup.push(function() { clearTimeout(timeout); }); + } +}; + +exports.exit = function(newState) { + _.forEachRight(cleanup, function(c) { c(); }); + cleanup = []; + + if (server.clients.length && !main.keep_alive) + main.shutdownWhenEmpty(); + + return true; +}; diff -Naur server-script.ori/states/lobby.js server-script/states/lobby.js --- server-script.ori/states/lobby.js 2016-02-07 14:33:04.000000000 +0100 +++ server-script/states/lobby.js 2016-02-07 16:18:05.520574028 +0100 @@ -1,1885 +1,1913 @@ -var main = require('main'); -var sim_utils = require('sim_utils'); -var server_utils = require('server_utils'); -var content_manager = require('content_manager'); -var utils = require('utils'); -var bouncer = require('bouncer'); -var env = require('env'); -var _ = require('thirdparty/lodash'); -var commander_manager = require('lobby/commander_manager'); -var color_manager = require('lobby/color_manager'); - -var getAIName = (function () { - - var ai_names = _.shuffle(require('ai_names_table').data); /* shuffle returns a new collection */ - - return function () { - var name = ai_names.shift(); - ai_names.push(name); - return name; - } -})(); - -var used_ai_ids = []; -var last_ai_number = 0; - -var getAIId = function () { - if (used_ai_ids.length) - return used_ai_ids.pop(); - else { - last_ai_number++; - return '' + last_ai_number; - } -} - -var returnAIId = function (id) { - used_ai_ids.push(id); -} - -var commanders = new commander_manager.CommanderManager(); -var colors = new color_manager.ColorManager(); - -var START_GAME_DELAY = 5; // In s. -var MAX_PLAYERS = 10; -var MAX_SPECTATORS = 3; -var MAX_CLIENTS = MAX_PLAYERS + MAX_SPECTATORS; -var DEFAULT_LOBBY_TAG = ''; -var DEFAULT_LOBBY_NAME = ''; -var DEFAULT_GAME_TYPE = 'FreeForAll'; -var VALID_GAME_TYPES = [DEFAULT_GAME_TYPE, 'TeamArmies', 'VersusAI']; -var isValidGameType = function (game_type) { - return VALID_GAME_TYPES.indexOf(game_type) != -1; -}; -var isFFAType = function (game_type) { - return game_type === 'FreeForAll'; -}; - -var DEFAULT_GAME_OPTIONS = { - dynamic_alliances: false, - dynamic_alliance_victory: false, - bounty_mode: false, - bounty_value: 0.5, - sandbox: false, - listen_to_spectators: false, - game_type: DEFAULT_GAME_TYPE, - land_anywhere: false, -}; - - -var alliance_groups = _.range(1, MAX_CLIENTS / 2 + 1); /* 0 indicates no alliance */ - -var debugging = false; - -function debug_log(object) { - if (debugging) - console.log(JSON.stringify(object,null,'\t')); -} - -var client_state = { - armies: [], - players: [], - colors: [], - system: {}, - settings: {}, - control: {} -}; - -/* the lobby stays up after we transition out of the state, so that it can handle login/rejoin attemps - if a player leaves the lobby, we kill the client (but they can still rejoin); however, if a player - leaves after the game has moved on to another state (usually due to a disconnect error), - we don't want to kill the client, since the playing state will setup a disconnect timer. */ -var hasStartedPlaying = false; - -function PlayerModel(client, options) { - var self = this; - - self.client = client; /* data, debugDesc, connected, id, name */ - try { - self.client_data = JSON.parse(client.data); - } catch (error) { - debug_log("Unable to parse client data for player"); - debug_log(error); - self.client_data = null; - } - self.creator = !!options.creator; - self.spectator = !!options.spectator; - - self.ai = !!options.ai; - self.personality = options.personality || ''; - - /* for now, only AI have landing policy. values: ['no_restriction', 'on_player_planet', 'off_player_planet'] */ - self.landingPolicy = (self.ai && options.landing_policy) ? options.landing_policy : 'no_restriction'; - - self.commander = commanders.getRandomDefaultCommanderSpec(); - - self.ready = self.ai ? true : false; - self.loading = self.ai ? false : true; - - self.armyIndex = -1; - self.slotIndex = -1; - - self.colorIndex = (self.ai || self.spectator) ? [-1, -1] : colors.takeRandomAvailableColor(); - - self.economyFactor = _.isFinite(options.economy_factor) ? options.economy_factor : 1.0; - self.economyFactor = Math.min(Math.max(0.0, self.economyFactor), 5.0); - - if (!!options.mod) - bouncer.addPlayerToModlist(client.id); - else - bouncer.removePlayerFromModlist(client.id); - - self.nextPrimaryColorIndex = function () { - if (self.spectator) - return; - - self.returnColorIndex(); - self.colorIndex = [colors.takeNextAvailableColorIndex(self.colorIndex[0]), 0]; - }; - - self.nextSecondaryColorIndex = function () { - if (self.spectator) - return; - - var colors = colors.getSecondaryColorsFor(self.colorIndex[0]); - var max = colors.getNumberOfColors(); - self.colorIndex[1] = (self.colorIndex[1] + 1) % max; - } - - self.clearColorIndex = function () { - self.returnColorIndex(); - self.colorIndex = [-1, -1]; - }; - - self.maybeTakeColorIndex = function () { - if (self.spectator || self.colorIndex[0] !== -1) - return; - self.colorIndex = colors.takeRandomAvailableColor(); - }; - - self.setPrimaryColorIndex = function (index) { - if (self.spectator) - return; - self.colorIndex = [colors.maybeGetNewColorIndex(self.colorIndex[0], index), self.colorIndex[1]]; - }; - - self.setSecondaryColorIndex = function (index) { - if (self.spectator) - return; - - self.colorIndex = [self.colorIndex[0], index]; - }; - - self.returnColorIndex = function () { - if (self.colorIndex[0] === -1) - return; - - colors.returnColorIndex(self.colorIndex[0]); - self.colorIndex = [-1, -1]; - }; - - self.adjustArmyIndexAboveTarget = function (target_index, delta) { - if (self.armyIndex > target_index) - self.armyIndex += delta; - }; - - self.adjustSlotIndexAboveTarget = function (target_index, delta) { - if (self.slotIndex > target_index) - self.slotIndex += delta; - }; - - self.processRemoveIndex = function (target_index) { - if (self.armyIndex === target_index) { - self.armyIndex = -1; - self.slotIndex = -1; - } - else - self.adjustArmyIndexAboveTarget(target_index, -1); - }; - - self.processRemoveSlotAtIndex = function (target_index, target_slot) { - if (self.armyIndex === target_index) { - - if (self.slotIndex === target_slot) { - self.armyIndex = -1; - self.slotIndex = -1; - } - else - self.adjustSlotIndexAboveTarget(target_slot, -1); - } - }; - - self.setAIPersonality = function (personality) { - if (!self.ai) - return; - - self.personality = personality; - }; - - self.setAILandingPolicy = function (policy) { - if (!self.ai) - return; - - self.landingPolicy = policy; - }; - - self.setEconomyFactor = function (value) { - value = Math.min(Math.max(0.0, value), 5.0); - self.economyFactor = value; - }; - - self.asJson = function () { - return { - name: self.client.name, - id: self.client.id, - ai: self.ai, - personality: self.personality, - landing_policy: self.landingPolicy, - economy_factor: self.economyFactor, - connected: self.ai ? true : self.client.connected, - creator: self.ai ? false : self.creator, - mod: self.ai ? false : bouncer.isPlayerMod(self.client.id), - army_index: self.armyIndex, - slot_index: self.slotIndex, - commander: self.commander, - ready: self.ready, - loading: self.loading, - color: colors.getColorFor(self.colorIndex), - color_index: self.colorIndex[0] - }; - }; - - self.finalize = function () { - return { - name: self.client.name, - commander: self.commander, - client: self.client, - army: self.armyIndex, - slot: self.slotIndex, - ai: self.ai, - personality: self.personality, - landing_policy: self.landingPolicy - }; - }; - - self.setCommander = function (new_commander) { - if (!new_commander || self.commander === new_commander) - return; - - var commanderObject = commanders.getCommanderObjectName(new_commander); - if (!commanderObject) { - debug_log("Failed to locate item " + (new_commander)); - return; - } - - if (!self.client.validateItem(commanderObject)) { - debug_log("Failed to validate ownership of " + (commanderObject)); - return; - } - - self.commander = new_commander; - - lobbyModel.updatePlayerState(); - } -}; - -function ArmyModel(options) { - var self = this; - - self.slots = options.slots ? Math.max(options.slots, 1) : 1; - self.alliance = !!options.alliance; - self.allianceGroup = 0; /* 0 indicates no alliance */ - - self.asJson = function () { - return { - slots: self.slots, - alliance: self.alliance - }; - }; - - self.finalizeAsConfig = function () { - var s = []; - s.length = self.slots; - - _.forEach(s, function (element, index) { - s[index] = 'player'; - }); - - return { - slots: s, - alliance_group: self.allianceGroup - }; - }; -}; - -function LobbyModel(creator) { - var self = this; - - self.maxNumberOfAllowedPlayers = 1; - self.players = {}; - self.armies = []; - self.system = {}; - self.minimalSystemDescription = {}; /* system sans custom planet source */ - self.config = {}; - self.settings = { - hidden: true, - game_options: _.cloneDeep(DEFAULT_GAME_OPTIONS), - required_content: content_manager.getRequiredContent(), - }; /* game_mode spectators broadcast_delay private friends public tag */ - self.control = {}; /* has_first_config starting system_ready sim_ready */ - - self.creator = creator; - - self.dirty = {}; - self.allDirty = { - control: true, - system: true, - players: true, - armies: true, - color: true, - settings: true, - beacon: true - }; - // These dirty flags (on the left) will be set when the associated flags (on the right) are set - self.chainDirty = { - beacon: ['players', 'armies', 'system', 'settings'], - color: ['players'] - }; - - // Set a given flag to "dirty" (e.g. self.setDirty({control: true}); ) - self.setDirty = function(flags) { - var needsApply = _.isEmpty(self.dirty); - _.assign(self.dirty, flags); - - while (!function() { - return _.all(self.chainDirty, function(flags, key) { - if (self.dirty[key]) - return true; - var chain = _.pick(self.dirty, flags); - if (_.isEmpty(chain)) - return true; - self.dirty[key] = true; - return false; - }); - }()); - - if (needsApply) - _.delay(function() { self.cleanDirtyFlags(); }); - }; - - // Custom functions for cleaning dirty flags. (broadcast to client for all other flags) - self.customCleaners = { - beacon: function () { self.updateBeacon(); } - }; - - // Clean all the dirty flags - self.cleanDirtyFlags = function () { - try { - _.forIn(self.dirty, function(yes, key) { - if (self.customCleaners.hasOwnProperty(key)) - self.customCleaners[key](); - else { - server.broadcast({ - message_type: key, - payload: client_state[key] - }); - } - }); - } - catch (e) { - // Note: Very important that the server "mostly" works if this happens. - console.error('Lobby unable to clean dirty flags:', e.toString()); - } - self.dirty = {}; - }; - - self.isCreator = function (id) { - return id === self.creator; - }; - - self.playersInArmy = function(army_index) { - return _.filter(_.values(self.players), function (element){ - return element.armyIndex === army_index; - }); - }; - - self.aiCount = function () { - return _.filter(_.values(self.players), function (element) { return element.ai }).length; - } - - self.totalSlots = function () { - - var result = 0; - _.forEach(self.armies,function (element) { - result += element.slots; - }); - - return result; - }; - - self.totalCurrentPlayers = function () { - var total = 0; - _.forEach(self.armies, function (element, index) { - total += self.playersInArmy(index).length; - }); - return total; - }; - - self.breakArmyIntoAlliances = function (army_index) { - var army = self.armies[army_index]; - if (!army) - return; - - var extra = army.slots - 1; - army.slots = 1; - var options = army.asJson(); - - var group = alliance_groups.splice(0, 1)[0]; - - _.invoke(self.players, 'adjustArmyIndexAboveTarget', army_index, extra); - - _.times(extra, function (index) { /* split armies */ - self.armies.splice(army_index, 0, new ArmyModel(options)); /* splice modifies the array */ - }); - - _.forEach(self.playersInArmy(army_index), function (element) { /* add players to new armies */ - element.armyIndex += element.slotIndex; - element.slotIndex = 0; - - self.armies[element.armyIndex].allianceGroup = group; - }); - } - - self.finalizeConfig = function () { - return { - "armies": _.invoke(self.armies, 'finalizeAsConfig'), - "system": self.system, - "enable_lan": true, - "spectators": 0, - "password": "", - "friends": [], - "blocked": [], - "public": true, - "players": self.totalCurrentPlayers(), - "vs_ai": false, - "game_options": self.settings.game_options - }; - } - - self.finalizePlayers = function () { - var result = {}; - - _.forIn(self.players, function (value, key) { - result[key] = value.finalize() - }); - - return result; - }; - - self.finalizeArmies = function () { - var result = _.invoke(self.armies, 'finalizeAsConfig'); - - _.forIn(self.players, function (element) { - var army = result[element.armyIndex]; - if (!army) /* player is spectating */ - return; - - army.slots[element.slotIndex] = element.finalize(); /* insert finalized block { name, commander, client, army, ai } */ - - if (element.slotIndex === 0) { /* only use the color for the first player in the army */ - result[element.armyIndex].color = colors.getColorFor(element.colorIndex); /* insert expanded color */ - result[element.armyIndex].color_index = element.colorIndex[0]; - result[element.armyIndex].econ_rate = element.economyFactor; - } - - if (element.ai) { - result[element.armyIndex].personality = element.personality; - result[element.armyIndex].landing_policy = element.landingPolicy; - } - }); - - return result; - } - - self.getFinalData = function () { - - while (true) - { - var target = _.findIndex(self.armies, function (element) { - return element.alliance && element.slots > 1; - }); - - if (target === -1) - break; - - self.breakArmyIntoAlliances(target); - } - - _.invoke(self.players, 'maybeTakeColorIndex'); - - return { - game: lobbyModel.finalizeConfig(), - armies: lobbyModel.finalizeArmies(), - players: lobbyModel.finalizePlayers(), - ranked: false - } - }; - - self.updatePlayerState = function () { - debug_log('updatePlayerState'); - var players = _.invoke(self.players, 'asJson'); - if (_.isEqual(client_state.players, players)) - return; - client_state.players = _.cloneDeep(players); - self.setDirty({players: true}); - }; - - self.updateArmyState = function () { - debug_log('updateArmyState'); - var armies = _.invoke(self.armies, 'asJson'); - if (_.isEqual(client_state.armies, armies)) - return; - client_state.armies = _.cloneDeep(armies); - self.setDirty({ armies: true }); - }; - - self.updateSystemState = function () { - debug_log('updateSystemState'); - if (_.isEqual(client_state.system, self.minimalSystemDescription)) - return; - client_state.system = _.cloneDeep(self.minimalSystemDescription); - self.setDirty({system: true}); - }; - - self.changeSystem = function (system) { - if (_.isEqual(system, self.system)) - return; - - self.changeControl({ system_ready: false, sim_ready: false }); - self.system = system; - - self.minimalSystemDescription = utils.getMinimalSystemDescription(self.system); - - self.updateSystemState(); - - /* this will take some time. the server will be unresponsive. */ - sim.shutdown(false); - sim.systemName = lobbyModel.system.name; - sim.planets = self.system.planets; - }; - - self.updateColorState = function () { - debug_log('updateColorState'); - if (_.isEqual(client_state.colors, colors.colors)) - return; - client_state.colors = _.cloneDeep(colors.colors); - self.setDirty({colors: true}); - }; - - self.updateControlState = function () { - debug_log('updateControlState'); - if (_.isEqual(client_state.control, self.control)) - return; - client_state.control = _.cloneDeep(self.control); - self.setDirty({control: true}); - }; - - self.changeControl = function (control /* has_first_config starting system_ready sim_ready */) { - _.assign(self.control, control); - self.updateControlState(); - }; - - self.updateSettingsState = function () { - debug_log('updateSettingsState'); - if (_.isEqual(client_state.settings, self.settings)) - return; - client_state.settings = _.cloneDeep(self.settings); - - main.spectators = Number(self.settings.spectators); - - self.setDirty({ settings: true }); - }; - - self.changeSettings = function (settings /* game_mode spectators hidden friends public tag */) { - _.assign(self.settings, settings); - self.updateSettingsState(); - }; - - self.updateClientState = function () { - debug_log('updateClientState'); - self.updatePlayerState(); - self.updateArmyState(); - self.updateSystemState(); - self.updateColorState(); - self.updateControlState(); - self.updateSettingsState(); - }; - - self.unreadyAllPlayers = function () { - debug_log('unreadyAllPlayers'); - _.forIn(self.players, function (element) { - if (element.ready && !element.ai) { - server.broadcastEventMessage(element.client.name, ' is no longer ready.'); - element.ready = false; - } - }); - - self.setDirty({ players: true }); - self.updatePlayerState(); - }; - - self.addPlayersToSlotsIfPossible = function () { - debug_log('addPlayersToSlotsIfPossible'); - - var army_index = 0; - _.forIn(self.players, function (element, key) { - - if (element.armyIndex !== -1 || element.spectator) - return; - - while (army_index < self.armies.length) - { - if (self.addPlayerToArmy(key, army_index)) - break; - army_index += 1; - } - }); - } - - self.addArmy = function (options) { - if (self.armies.length >= MAX_PLAYERS) - return; - - if (options.slots && options.slots + self.totalSlots() > MAX_PLAYERS) - return; - - self.unreadyAllPlayers(); - - self.armies.push(new ArmyModel(options)); - self.addPlayersToSlotsIfPossible(); - self.updateArmyState(); - }; - - self.removeArmy = function (army_index) { - var spares = []; - - self.unreadyAllPlayers(); - - self.armies.splice(army_index, 1); - - _.forEach(self.players, function (element) { - if (element.ai && element.armyIndex === army_index) - spares.push(element.client.id) - }); - - _.forEach(spares, self.removePlayer); - - _.invoke(self.players, 'processRemoveIndex', army_index); - - self.addPlayersToSlotsIfPossible(); - self.updateArmyState(); - self.updatePlayerState(); - }; - - self.modifyArmy = function (army_index, options) { - debug_log('modifyArmy'); - - var spares = []; - - var army = self.armies[army_index]; - if (!army) - return; - - var new_options = _.assign(army.asJson(), options); - - var ai = !!_.filter(self.playersInArmy(army_index), function (element) { return element.ai }).length; - if (ai) - new_options.alliance = true; /* override to prevent shared army with ai */ - - if (options.slots && (options.slots - army.slots + self.totalSlots()) > MAX_PLAYERS) - return; - - self.unreadyAllPlayers(); - - if (options.slots < army.slots) { - _.forEach(_.range(options.slots, army.slots), function (index) { - _.invoke(self.players, 'processRemoveSlotAtIndex', army_index, index); - - _.forEach(self.players, function (element) { - if (element.ai && element.armyIndex === army_index && element.slotIndex == index) - spares.push(element.client.id) - }); - }); - } - - _.forEach(spares, self.removePlayer); - - self.armies[army_index] = new ArmyModel(new_options); - - self.addPlayersToSlotsIfPossible(); - - self.fixColors(); - - self.updateArmyState(); - self.updateColorState(); - self.updatePlayerState(); - }; - - self.numPlayerSlots = function() { - return utils.sum(self.armies, function(army) { return army.ai ? 0 : army.slots; }); - }; - - self.numPlayers = function () { - return _.keys(self.players).length; - }; - - self.updateBeacon = function () { - debug_log('updateBeacon'); - var numPlayerSlots = self.numPlayerSlots(); - server.maxClients = Math.min(MAX_PLAYERS, numPlayerSlots) + Math.min(MAX_SPECTATORS, main.spectators); - var publish = !server.closed && (self.settings.public || bouncer.getWhitelist().length) && !self.settings.hidden; - - if (publish) { - var full = server.clients.length >= server.maxClients; - - var modNames = []; - var mods = server.getMods(); - if (mods !== undefined && mods.mounted_mods !== undefined) { - _.forEach(mods.mounted_mods, function (element) { - modNames.push(element.display_name); - }); - } - - var player_names = _.map(_.filter(self.players, { 'spectator': false }), function (player) { return player.client.name; }); - var spectator_names = _.map(_.filter(self.players, { 'spectator': true }), function (player) { return player.client.name; }); - - var mode = DEFAULT_GAME_TYPE; - if (self.settings.game_options) - mode = self.settings.game_options.game_type; - - server.beacon = { - uuid: server.uuid(), - full: full, - started: self.control.countdown || self.control.starting, - players: player_names.length, - creator: self.creatorName(), - max_players: numPlayerSlots, - spectators: spectator_names.length, - max_spectators: main.spectators, - mode: mode, - mod_names: modNames, - cheat_config: main.cheats, - player_names: player_names, - spectator_names: spectator_names, - require_password: !!bouncer.doesGameRequirePassword(), - whitelist: bouncer.getWhitelist(), - blacklist: bouncer.getBlacklist(), - tag: self.settings.tag, - game: { - system: self.minimalSystemDescription, - name: self.settings.game_name - }, - required_content: content_manager.getRequiredContent(), - bounty_mode: self.settings.game_options ? !!self.settings.game_options.bounty_mode : false, - bounty_value: self.settings.game_options ? self.settings.game_options.bounty_value : 0.5, - sandbox: self.settings.game_options ? !!self.settings.game_options.sandbox : false - }; - } else { - server.beacon = null; - } - }; - - self.addPlayer = function (client, options) { - debug_log('addPlayer'); - var player = new PlayerModel(client, options); - self.players[client.id] = player; - self.updatePlayerState(); - self.updateColorState(); - - if (!options.ai) { - _.delay(server.broadcastEventMessage, 500, player.client.name, ' joined the lobby.'); - - debug_log('Player Stats: '); - debug_log(player.client.statistics); - client.incrementStatistic("TestStat_LobbiesJoined", 1); - } - }; - - self.addAI = function (payload) { - var player_id = getAIId(); - var success = false; - - if (!payload) - return; - - self.addPlayer({ connected: true, id: player_id, name: getAIName() }, payload.options); - - success = self.addPlayerToArmy(player_id, payload.army_index); - if (!success) - self.removePlayer(player_id); - }; - - self.creatorName = function () { - var player = self.players[self.creator]; - return player ? player.client.name : ''; - }; - - self.chooseNextPlayerAsCreator = function () { - debug_log('chooseNextPlayerAsCreator'); - if (_.isEmpty(self.players)) - return; - - var client = _.first(server.clients); - if (!client) - return; - - var id = client.id; - self.creator = id; - - var player = self.players[id]; - - player.creator = true; - bouncer.addPlayerToModlist(id); - - self.updatePlayerState(); - server.broadcastEventMessage(player.client.name, ' is now the host.'); - }; - - self.removePlayer = function (id) { - debug_log('removePlayer'); - - delete client_state.players[id]; - var player = self.players[id]; - - if (player) { - self.removePlayerFromArmy(id, { clear_color: true }); - - var ai = player.ai; - if (!ai) - server.broadcastEventMessage(player.client.name, ' has left the lobby.'); - - player.returnColorIndex(); - delete self.players[id]; - - if (!ai) { - console.log('killing client ' + id); - player.client.kill(); - - // Terminate empty lobbies - if (!main.keep_alive && !server.connected) { - sim.shutdown(false); - server.exit(); - return; - } - - if (id === self.creator) - self.chooseNextPlayerAsCreator(); - } - - if (ai) - returnAIId(id); - - self.updatePlayerState(); - self.updateColorState(); - } - }; - - self.kickPlayer = function (id) { - debug_log('kickPlayer'); - bouncer.addPlayerToBlacklist(id); - self.removePlayer(id); - }; - - self.addPlayerToArmy = function (player_id, army_index) { - debug_log('addPlayerToArmy'); - var player = self.players[player_id]; - var army = self.armies[army_index]; - - var array = self.playersInArmy(army_index); - var count = array.length; - - if (!player || !army || count >= army.slots) - return false; - - if (player.armyIndex === army_index) - return false; - - player.armyIndex = army_index; - player.slotIndex = count; - player.spectator = false; - - if (player.ai) - army.alliance = true; /* override to prevent shared army with ai */ - - self.fixColors(); - - self.updatePlayerState(); - self.updateArmyState(); - self.updateColorState(); - return true; - }; - - self.removePlayerFromArmy = function (player_id, options) { - debug_log('removePlayerFromArmy'); - var player = self.players[player_id]; - - if (!player) - return false; - - if (options && options.clear_color) - player.clearColorIndex(); - - var army_index = player.armyIndex; - var slot_index = player.slotIndex; - - player.armyIndex = -1; - player.slotIndex = -1; - - if (options && options.set_spectator) - player.spectator = true; - - /* move players down to fill empty slot */ - _.invoke(self.players, 'processRemoveSlotAtIndex', army_index, slot_index); - - self.fixColors(); - - self.updatePlayerState(); - self.updateArmyState(); - self.updateColorState(); - - return true; - } - - /* this checks every army, which is excessive; however, it is very reliable. */ - self.fixColors = function () { - _.forEach(self.armies, function (element, index) { - self.fixColorsForArmy(index); - }); - } - - /* it would be preferrable to just call this function for each modified army */ - self.fixColorsForArmy = function (army_index) { - - var army = self.armies[army_index]; - - if (!army) - return; - - _.forEach(self.playersInArmy(army_index), function (element, index) { - if (index === 0 || army.alliance) - element.maybeTakeColorIndex(); - else - element.returnColorIndex(); /* only the first player in a shared army gets a color */ - }); - - self.setDirty({ colors: true }); - }; - - self.setPrimaryColorIndex = function (player_id, index, ai) { - debug_log('{{setPrimaryColorIndex}} ' + index); - var player = self.players[player_id]; - - if (!player || player.ai !== ai || !colors.isValidPrimaryColorIndex(index)) - return; - - player.setPrimaryColorIndex(index); - - self.updatePlayerState(); - self.updateColorState(); - }; - - self.setSecondaryColorIndex = function (player_id, index, ai) { - debug_log('{{setSecondaryColorIndex}} ' + index); - var player = self.players[player_id]; - - if (!player || player.ai !== ai || !colors.isValidColorPair(player.colorIndex[0], index)) - return; - - player.setSecondaryColorIndex(index); - - self.updatePlayerState(); - self.updateColorState(); - }; - - self.setAIPersonality = function (player_id, personality) { - debug_log('setAIPersonality'); - var player = self.players[player_id]; - - if (!player || !player.ai) - return; - - player.setAIPersonality(personality); - - self.unreadyAllPlayers(); - self.updatePlayerState(); - }; - - self.setAILandingPolicy = function (player_id, policy) { - debug_log('setAILandingPolicy'); - var player = self.players[player_id]; - - if (!player || !player.ai) - return; - - player.setAILandingPolicy(policy); - - self.unreadyAllPlayers(); - self.updatePlayerState(); - }; - - self.setEconomyFactor = function (player_id, value) { - debug_log('setEconomyFactor'); - var player = self.players[player_id]; - - if (!player) - return; - - if (player.economyFactor != value) - { - player.setEconomyFactor(value); - - self.unreadyAllPlayers(); - self.updatePlayerState(); - } - }; - - self.validateSetup = function () { - debug_log('validateSetup'); - var totalOpenSlots = self.numPlayerSlots(); - var totalPlayersInSlots = utils.sum(self.players, function (player) { return player.armyIndex >= 0; }); - - debug_log('Validation:'+ totalPlayersInSlots+ '/'+ totalOpenSlots); /* todo: add validation stage to control state */ - - if (totalPlayersInSlots !== totalOpenSlots) - return 'Empty slots encountered'; - }; - - self.gameType = function() { - if (!self.settings.game_options) - return null; - return self.settings.game_options.game_type; - }; -}; - -var lobbyModel; - -var cleanup = []; -var cleanupOnEntry = []; - -function allowChangesFrom(client) { - if (!client || !lobbyModel.isCreator(client.id)) - return false; - if (lobbyModel.control.countdown) - return false; - if (lobbyModel.control.starting) - return false; - - return true; -} - -function playerMsg_resetArmies(msg){ - debug_log('playerMsg_resetArmies'); - var response = server.respond(msg); - var payload = msg.payload; - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - _.forEach(lobbyModel.players, function (element, key) { - if (element.ai) - lobbyModel.removePlayer(key); - else - lobbyModel.removePlayerFromArmy(key); - }); - - lobbyModel.armies = []; - _.forEach(payload, function (element) { - lobbyModel.addArmy(element); - }); - - lobbyModel.changeControl({ has_first_config: true }); - - response.succeed(); -}; - -function playerMsg_addArmy(msg /* client payload */){ - debug_log('playerMsg_addArmy'); - var response = server.respond(msg); - var payload = msg.payload; - - if (!payload.options) - return response.fail("Not allowed."); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.addArmy(payload.options); - - response.succeed(); -} - -function playerMsg_removeArmy(msg){ - debug_log('playerMsg_removeArmy'); - var response = server.respond(msg); - var payload = msg.payload; - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.removeArmy(payload.army_index); - - response.succeed(); -} - -function playerMsg_addAI(msg) { - debug_log('playerMsg_addAI'); - - var response = server.respond(msg); - var payload = msg.payload; - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.addAI(payload); - - response.succeed(); -} - -function playerMsg_modifySystem(msg) { - debug_log('modifySystem'); - var response = server.respond(msg); - var payload = msg.payload; - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - var systemValidationResult = sim_utils.validateSystemConfig(msg.payload); - if (_.isString(systemValidationResult)) { - debug_log('Invalid system'); - return response.fail("Invalid system provided - " + systemValidationResult); - } - else - systemValidationResult.then( function () { - lobbyModel.changeSystem(msg.payload); - response.succeed(); - }); -} - -function playerMsg_modifyArmy(msg){ - debug_log('modifyArmy'); - var response = server.respond(msg); - var payload = msg.payload; - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.modifyArmy(payload.army_index, payload.options); - - response.succeed(); -} - -function updateBouncer(config) { - - if (config.password || bouncer.doesGameRequirePassword()) - bouncer.setPassword(config.password); - - bouncer.clearWhitelist(); - var hasFriendsList = config.friends && config.friends.length; - if (hasFriendsList) { - _.forEach(config.friends, function (element) { bouncer.addPlayerToWhitelist(element); }); - } - - bouncer.clearBlacklist(); - var hasBlockList = config.blocked && config.blocked.length; - if (hasBlockList) { - _.forEach(config.blocked, function (element) { bouncer.addPlayerToBlacklist(element); }); - } -} - - -function playerMsg_modifyBouncer(msg) { - debug_log('playerMsg_modifyBouncer'); - - var config = msg.payload; - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - updateBouncer(config); - - lobbyModel.setDirty({ beacon: true }); - - response.succeed(); -} - -function playerMsg_modifySettings(msg) { - - debug_log('playerMsg_modifySettings'); - var config = msg.payload; - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) { - lobbyModel.setDirty({ settings: true }); - return response.fail("Not allowed."); - } - - var game_options = _.cloneDeep(DEFAULT_GAME_OPTIONS); - - var settings = { - spectators: MAX_SPECTATORS, - hidden: false, - friends: false, - public: true, - tag: DEFAULT_LOBBY_TAG, - game_name: DEFAULT_LOBBY_NAME, - required_content: content_manager.getRequiredContent(), - }; - - if (config.game_options) - game_options.game_type = config.game_options.game_type; - else if (client_state.settings && client_state.settings.game_options) - game_options.game_type = client_state.settings.game_options.game_type; - - if (!isValidGameType(game_options.game_type)) - game_options.game_type = DEFAULT_GAME_TYPE; - - updateBouncer(config); - var hasFriendsList = bouncer.getWhitelist().length; - - if (config.game_options) - { - game_options.land_anywhere = !!config.game_options.land_anywhere; - - if (isFFAType(game_options.game_type)) { - game_options.dynamic_alliances = !!config.game_options.dynamic_alliances; - if (game_options.dynamic_alliances) - game_options.dynamic_alliance_victory = !!config.game_options.dynamic_alliance_victory; - } - if (config.game_options.bounty_mode) - game_options.bounty_mode = _.contains(content_manager.getRequiredContent(), 'PAExpansion1') && !!config.game_options.bounty_mode; - if (config.game_options.bounty_value) - game_options.bounty_value = config.game_options.bounty_value; - if (config.game_options.sandbox) - game_options.sandbox = !!config.game_options.sandbox; - if (config.game_options.listen_to_spectators) - game_options.listen_to_spectators = !!config.game_options.listen_to_spectators; - } - - settings.hidden = (!hasFriendsList && !config.public); - settings.friends = !!hasFriendsList; - settings.public = (config.public && !hasFriendsList); - if (config.tag) - settings.tag = config.tag; - - settings.spectators = Math.min(Number(config.spectators), MAX_SPECTATORS); - - if (_.isString(config.game_name)) - settings.game_name = config.game_name.substring(0, Math.min(config.game_name.length, 128)); - - _.forEach(_.keys(lobbyModel.settings.game_options), function (key) { - if (lobbyModel.settings.game_options[key] !== game_options[key]) { - var name = key.replace(/_/g, ' '); - var message = name + ' ' + - (_.isBoolean(game_options[key]) - ? (!!game_options[key] ? 'enabled' : 'disabled') - : ('changed')) - + '.'; - server.broadcastEventMessage('', message, 'settings'); - } - }); - - settings.game_options = game_options; - - var nameChangeOnly = false; - - if (settings.game_name !== lobbyModel.settings.game_name) { - var currentSettings = _.cloneDeep(lobbyModel.settings); - delete currentSettings.game_name; - var newSettings = _.cloneDeep(settings); - delete newSettings.game_name; - - nameChangeOnly = _.isEqual(currentSettings, newSettings); - } - - lobbyModel.changeSettings(settings); - - if (!nameChangeOnly) { - lobbyModel.unreadyAllPlayers(); - } - - response.succeed(); -} - -function maybeStartLandingState() { - debug_log('maybeStartLandingState'); - if (!lobbyModel.control.starting || !lobbyModel.control.system_ready || !lobbyModel.control.sim_ready) - return; - - lobbyModel.updateClientState(); - - var final_data = lobbyModel.getFinalData(); - - try { - if (server_utils.log_lobby_description) { - console.log('final lobby data:'); - console.log(JSON.stringify(final_data, null, '\t')); - } - } - catch (e) { - console.log('final lobby data: failed.'); // this is *not* expected. - }; - - hasStartedPlaying = true; - main.setState(main.states.landing, final_data); -} - - - -function playerMsg_startCountdown(msg) { - debug_log('playerMsg_startGame'); - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - var player = lobbyModel.players[msg.client.id]; - player.ready = true; - - function not_ready (){ - return _.some(lobbyModel.players, function (value) { - return !value.ready && !value.spectator; - }); - } - - if (not_ready()) { - player.ready = false; - return response.fail("Not ready."); - - } - - var lobbyValidationResult = lobbyModel.validateSetup(); - if (lobbyValidationResult) { - player.ready = false; - return response.fail("Invalid game setup - " + lobbyValidationResult); - } - - if (!lobbyModel.control.sim_ready) { - player.ready = false; - return response.fail("Server is not done gerating planets"); - } - - lobbyModel.updatePlayerState(); - lobbyModel.changeControl({ countdown: true }); - - function startGame() { - - server.broadcastEventMessage('', -1, 'countdown'); - - server.broadcastEventMessage('', 'Game is starting.'); - - lobbyModel.changeControl({ starting: true }); - maybeStartLandingState(); - } - - var count = _.has(msg, 'countdown') ? msg.countdown : START_GAME_DELAY; - function countdownToStartGame() { - server.broadcastEventMessage('', count, 'countdown'); - count -= 1; - - if (count > 0) - setTimeout(countdownToStartGame, 1000); - else - setTimeout(startGame, 1000); - } - - if (lobbyModel.totalCurrentPlayers() < 2) - startGame(); - else { - server.broadcastEventMessage('', 'Game will start in ' + START_GAME_DELAY + ' seconds.'); - countdownToStartGame(); - } - - response.succeed(); -} - -function playerMsg_setPrimaryColorIndex(msg) { - debug_log('playerMsg_setPrimaryColorIndex'); - debug_log(msg); - var response = server.respond(msg); - - lobbyModel.setPrimaryColorIndex(msg.client.id, msg.payload, false); - response.succeed(); -} - -function playerMsg_setPrimaryColorIndexForAI(msg) { - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.setPrimaryColorIndex(msg.payload.id, msg.payload.color, true); - response.succeed(); -} - -function playerMsg_setSecondaryColorIndex(msg) { - debug_log('playerMsg_setSecondaryColorIndex'); - debug_log(msg); - var response = server.respond(msg); - - lobbyModel.setSecondaryColorIndex(msg.client.id, msg.payload, false); - response.succeed(); -} - -function playerMsg_setSecondaryColorIndexForAI(msg) { - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.setSecondaryColorIndex(msg.payload.id, msg.payload.color, true); - response.succeed(); -} - -function playerMsg_setAIPersonality(msg) { - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.setAIPersonality(msg.payload.id, msg.payload.ai_personality); - response.succeed(); -} - -function playerMsg_setAILandingPolicy(msg) { - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.setAILandingPolicy(msg.payload.id, msg.payload.ai_landing_policy); - response.succeed(); -} - -function playerMsg_setEconomyFactor(msg) { - var response = server.respond(msg); - - if (!allowChangesFrom(msg.client)) - return response.fail("Not allowed."); - - lobbyModel.setEconomyFactor(msg.payload.id, msg.payload.economy_factor); - response.succeed(); -} - -function playerMsg_updateCommander(msg) { - debug_log('playerMsg_updateCommander'); - var response = server.respond(msg); - var player = lobbyModel.players[msg.client.id]; - - if (!msg.payload || !msg.payload.commander || !player) - return response.fail("Invalid message"); - - player.setCommander(msg.payload.commander); - - return response.succeed(); -} - -function playerMsg_joinArmy(msg) { - debug_log('playerMsg_joinArmy'); - var response = server.respond(msg); - var player = lobbyModel.players[msg.client.id]; - - if (!msg.payload || !player) - return response.fail("Invalid message"); - - if (msg.payload.commander) - player.setCommander(msg.payload.commander); - - lobbyModel.removePlayerFromArmy(msg.client.id); - if (lobbyModel.addPlayerToArmy(msg.client.id, msg.payload.army)) { - server.broadcastEventMessage(player.client.name, ' has joined an army.'); - response.succeed(); - } else { - response.fail("Unable to add player to army"); - } -} - -function playerMsg_toggleReady(msg) { - debug_log('playerMsg_toggleReady'); - - var response = server.respond(msg); - var player = lobbyModel.players[msg.client.id]; - - if (!player) - return response.fail("Invalid message"); - - if (client_state.control.countdown) - return response.fail("Cannot change ready after countdown has started."); - - player.ready = !player.ready; - - lobbyModel.updatePlayerState(); - - server.broadcastEventMessage(player.client.name, player.ready ? ' is now ready.' : ' is no longer ready.'); - - response.succeed(); -} - -function playerMsg_leaveArmy(msg) { - debug_log('playerMsg_leaveArmy'); - var response = server.respond(msg); - - var player = lobbyModel.players[msg.client.id]; - if (!player) - return; - - if (lobbyModel.removePlayerFromArmy(msg.client.id, { clear_color: true, set_spectator: true })) { - server.broadcastEventMessage(player.client.name, ' is now a spectator.'); - response.succeed(); - } else { - response.fail('Could not remove you from the army'); - } -} - -function playerMsg_chatMessage(msg) { - debug_log('playerMsg_chatMessage'); - var response = server.respond(msg); - if (!msg.payload || !msg.payload.message) - return response.fail("Invalid message"); - server.broadcast({ - message_type: 'chat_message', - payload: { - player_name: msg.client.name, - message: msg.payload.message - } - }); - response.succeed(); -} - -function playerMsg_leave(msg) { - debug_log('playerMsg_leave'); - var response = server.respond(msg); - - lobbyModel.removePlayer(msg.client.id); - - response.succeed(); -} - -function playerMsg_kick(msg) { - debug_log('playerMsg_kick'); - debug_log(msg); - var response = server.respond(msg); - - var id = msg.payload.id; - var player = lobbyModel.players[id]; - - if (!bouncer.isPlayerMod(msg.client.id)) - return response.fail("Only mods can kick."); - - if (bouncer.isPlayerMod(id)) - return response.fail("Mods cannot be kicked."); - - if (!player) - return response.fail("Already left"); - - bouncer.addPlayerToBlacklist(id); - - lobbyModel.kickPlayer(id); - - response.succeed(); -} - -function playerMsg_promoteToMod(msg) { - debug_log('playerMsg_promoteToMod'); - var response = server.respond(msg); - var id = msg.payload.id; - var player = lobbyModel.players[id]; - - if (!bouncer.isPlayerMod(msg.client.id)) - return response.fail("Only mods can promote."); - - if (!player) - return response.fail("Player is absent"); - response.succeed(); - - bouncer.addPlayerToModlist(id); - lobbyModel.updatePlayerState(); -} - -function playerMsg_setLoading(msg) { - debug_log('playerMsg_setLoading'); - - var response = server.respond(msg); - var player = lobbyModel.players[msg.client.id]; - - if (!player) - return response.fail("Invalid message"); - - player.loading = msg.payload.loading; - - lobbyModel.updatePlayerState(); - - response.succeed(); -} - -function playerMsg_modDataAvailable(msg) { - debug_log('playerMsg_modDataAvailable'); - - var response = server.respond(msg); - - if (lobbyModel.creator !== msg.client.id) { - return response.fail("Mod data can only be provided by lobby creator"); - } - - var auth_token = ""; - var hasMods = false; - var mods = server.getMods(); - if (mods !== undefined) { - auth_token = mods.auth_token || ""; - if (mods.mounted_mods !== undefined && mods.mounted_mods.length > 0) { - hasMods = true; - } - } - if (hasMods) { - return response.fail("Mod data is already mounted"); - } - - response.succeed({ "auth_token": auth_token }); -} - -function check_cheat(cheatname, callback) { - file.load('/server-script/modroot/cheat_' + cheatname + '.json', function (data) { - var cheat_enabled = false; - if (data !== undefined && data.length > 0) { - var config = JSON.parse(data); - if (config.cheat_flags !== undefined) { - if (config.cheat_flags[cheatname]) { - main.cheats.cheat_flags[cheatname] = true; - main.cheats.cheat_flags.any_enabled = true; - main.cheats.cheat_flags.cheat_mod_enabled = true; - cheat_enabled = true; - console.log('CHEATS: Mod enabled cheat: ' + cheatname); - - server.broadcast({ - message_type: 'set_cheat_config', - payload: main.cheats - }); - } - } - } - if (callback !== undefined) { - callback(cheat_enabled); - } - }); -} - -function playerMsg_modDataUpdated(msg) { - check_cheat('allow_change_vision'); - check_cheat('allow_change_control'); - check_cheat('allow_create_unit'); - check_cheat('allow_mod_data_updates', function (cheat_enabled) { - if (cheat_enabled) { - server.disableWriteReplay(); - } else { - server.resetModUpdateAuthToken(); - } - }); - - _.forEach(server.clients, function (client) { - if (client.id !== msg.client.id) { - client.downloadModsFromServer(); - } else { - client.message({ - message_type: 'mount_mod_file_data', - payload: {} - }); - } - }); -} - -function playerMsg_requestCheatConfig(msg) { - debug_log('playerMsg_requestCheatConfig'); - - var response = server.respond(msg); - - response.succeed({ "cheat_config": main.cheats }); -} - -function initOwner(owner) { - debug_log('initOwner'); - server.maxClients = 1; - - if (!owner) { - var testConfig = _.cloneDeep(require('test').exampleConfig); - main.setState(main.states.lobby, testConfig); - return; - } - - bouncer.addPlayerToModlist(owner.id); - - var client_data = { uberid: '', password: '', uuid: '' }; - try { - client_data = JSON.parse(owner.data); - bouncer.setUUID(client_data.uuid); - } - catch (error) { - debug_log('js initOwner : unable to parse owner data'); - } -} - -exports.url = 'coui://ui/main/game/new_game/new_game.html'; -exports.enter = function (owner) { - - _.forEachRight(cleanupOnEntry, function (c) { c(); }); - - lobbyModel = new LobbyModel(); - cleanupOnEntry.push(function () { lobbyModel = undefined; }); - - initOwner(owner); - lobbyModel.changeControl({ has_first_config: false, countdown: false, starting: false, system_ready: false, sim_ready: false }); - - utils.pushCallback(sim.planets, 'onReady', function (onReady) { - debug_log('sim.planets.onReady'); - lobbyModel.changeControl({ system_ready: true }); - sim.create(); - maybeStartLandingState(); - return onReady; - }); - cleanup.push(function () { sim.planets.onReady.pop(); }); - - utils.pushCallback(sim, 'onReady', function (onReady) { - debug_log('sim.onReady'); - lobbyModel.changeControl({ sim_ready: true }); - maybeStartLandingState(); - return onReady; - }); - cleanup.push(function () { sim.onReady.pop(); }); - - utils.pushCallback(server, 'onConnect', function (onConnect, client, reconnect) { - debug_log('onConnect'); - var client_data = { uberid: '', password: '', uuid: '' }; - try { - client_data = JSON.parse(client.data); - debug_log(client); - } - catch (e) { - debug_log('js utils.pushCallback : unable to parse client.data'); - server.rejectClient(client, 'Bad Client data'); - return onConnect; - } - - if (!bouncer.isPlayerValid(client_data.uberid, client_data.password, client_data.uuid, lobbyModel.settings.public)) { - console.log('invalid credentials'); - server.rejectClient(client, 'Credentials are invalid'); - return onConnect; - } - - if (!reconnect) { - var max = Math.min(MAX_CLIENTS, lobbyModel.numPlayerSlots() + main.spectators); - if (lobbyModel.numPlayers() >= max) { - console.error('lobby at capacity. client rejected.'); - server.rejectClient(client, 'No room'); - return onConnect; - } - } - - utils.pushCallback(client, 'onDisconnect', function (onDisconnect) { - if (!hasStartedPlaying) { /* don't kill the client unless we have not started any other states. */ - console.log('removing disconnected player from the lobby.'); - lobbyModel.removePlayer(client.id); - } - return onDisconnect; - }); - cleanup.push(function () { - if (client.onDisconnect) { - console.log('remove disconnect handler'); - client.onDisconnect.pop(); - } - /* removePlayer calls client.kill(), which will destroy the onDisconnect handler */ - }); - - var players = _.filter(lobbyModel.players, { 'spectator': false }); - var options = { mod: bouncer.isPlayerMod(client.id), creator: client.id === lobbyModel.creator }; - /* force the player to be a spectator */ - if (players.length >= MAX_PLAYERS) - options.spectator = true; - - if (!lobbyModel.players.hasOwnProperty(client.id)) - lobbyModel.addPlayer(client, options); - else - lobbyModel.updatePlayerState(); - - lobbyModel.addPlayersToSlotsIfPossible(); - - var player = lobbyModel.players[client.id]; - if (player.armyIndex === -1) /* make the player a spectator if there is no room */ - player.spectator = true; - - debug_log('calling client.downloadModsFromServer'); - client.downloadModsFromServer(); - - client.message({ - message_type: 'set_cheat_config', - payload: main.cheats - }); - - return onConnect; - }); - cleanupOnEntry.push(function () { server.onConnect.pop(); }); - - lobbyModel.creator = owner.id; - lobbyModel.addPlayer(owner, { mod: true, creator: true }); - bouncer.addPlayerToModlist(owner.id); - - _.forEach(server.clients, function (client) { - if (client !== owner) { - lobbyModel.addPlayer(client, { mod: false, creator: false }); - } - }); - - var removeHandlers = server.setHandlers({ - reset_armies: playerMsg_resetArmies, - add_army: playerMsg_addArmy, - remove_army: playerMsg_removeArmy, - add_ai: playerMsg_addAI, - modify_system: playerMsg_modifySystem, - modify_army: playerMsg_modifyArmy, - modify_bouncer: playerMsg_modifyBouncer, - modify_settings: playerMsg_modifySettings, - start_game: playerMsg_startCountdown, - set_primary_color_index: playerMsg_setPrimaryColorIndex, - set_primary_color_index_for_ai: playerMsg_setPrimaryColorIndexForAI, - set_secondary_color_index: playerMsg_setSecondaryColorIndex, - set_secondary_color_index_for_ai: playerMsg_setSecondaryColorIndexForAI, - set_ai_personality: playerMsg_setAIPersonality, - set_ai_landing_policy: playerMsg_setAILandingPolicy, - set_econ_factor: playerMsg_setEconomyFactor, - join_army: playerMsg_joinArmy, - toggle_ready: playerMsg_toggleReady, - leave_army: playerMsg_leaveArmy, - update_commander: playerMsg_updateCommander, - chat_message: playerMsg_chatMessage, - leave: playerMsg_leave, - kick: playerMsg_kick, - promote_to_mod: playerMsg_promoteToMod, - set_loading: playerMsg_setLoading, - mod_data_available: playerMsg_modDataAvailable, - mod_data_updated: playerMsg_modDataUpdated, - request_cheat_config: playerMsg_requestCheatConfig - }); - cleanup.push(function () { removeHandlers(); }); - - lobbyModel.updateBeacon(); - - return client_state; -}; - -exports.exit = function (newState) { - _.forEachRight(cleanup, function (c) { c(); }); - cleanup = []; - - return true; -}; - -main.gameModes.lobby = exports; -// This is for backwards compatibility. Game used to ask for 'Config' game mode. -main.gameModes.Config = exports; +var main = require('main'); +var sim_utils = require('sim_utils'); +var server_utils = require('server_utils'); +var content_manager = require('content_manager'); +var utils = require('utils'); +var bouncer = require('bouncer'); +var env = require('env'); +var _ = require('thirdparty/lodash'); +var commander_manager = require('lobby/commander_manager'); +var color_manager = require('lobby/color_manager'); + +var getAIName = (function () { + + var ai_names = _.shuffle(require('ai_names_table').data); /* shuffle returns a new collection */ + + return function () { + var name = ai_names.shift(); + ai_names.push(name); + return name; + } +})(); + +var used_ai_ids = []; +var last_ai_number = 0; + +var getAIId = function () { + if (used_ai_ids.length) + return used_ai_ids.pop(); + else { + last_ai_number++; + return '' + last_ai_number; + } +} + +var returnAIId = function (id) { + used_ai_ids.push(id); +} + +var commanders = new commander_manager.CommanderManager(); +var colors = new color_manager.ColorManager(); + +var START_GAME_DELAY = 5; // In s. +var MAX_PLAYERS = main.MAX_PLAYERS; +var MAX_SPECTATORS = main.MAX_SPECTATORS; +var MAX_CLIENTS = MAX_PLAYERS + MAX_SPECTATORS; +var DEFAULT_LOBBY_TAG = ''; +var DEFAULT_LOBBY_NAME = ''; +var DEFAULT_GAME_TYPE = 'FreeForAll'; +var VALID_GAME_TYPES = [DEFAULT_GAME_TYPE, 'TeamArmies', 'VersusAI']; +var isValidGameType = function (game_type) { + return VALID_GAME_TYPES.indexOf(game_type) != -1; +}; +var isFFAType = function (game_type) { + return game_type === 'FreeForAll'; +}; + +var DEFAULT_GAME_OPTIONS = { + dynamic_alliances: false, + dynamic_alliance_victory: false, + bounty_mode: false, + bounty_value: 0.5, + sandbox: false, + listen_to_spectators: false, + game_type: DEFAULT_GAME_TYPE, + land_anywhere: false, +}; + + +var alliance_groups = _.range(1, MAX_CLIENTS / 2 + 1); /* 0 indicates no alliance */ + +var debugging = false; + +function debug_log(object) { + if (debugging) + console.log(JSON.stringify(object,null,'\t')); +} + +var client_state = { + armies: [], + players: [], + colors: [], + system: {}, + settings: {}, + control: {} +}; + +/* the lobby stays up after we transition out of the state, so that it can handle login/rejoin attempts + if a player leaves the lobby, we kill the client (but they can still rejoin); however, if a player + leaves after the game has moved on to another state (usually due to a disconnect error), + we don't want to kill the client, since the playing state will setup a disconnect timer. */ +var hasStartedPlaying = false; + +function PlayerModel(client, options) { + var self = this; + + self.client = client; /* data, debugDesc, connected, id, name */ + try { + self.client_data = JSON.parse(client.data); + } catch (error) { + debug_log("Unable to parse client data for player"); + debug_log(error); + self.client_data = null; + } + self.creator = !!options.creator; + self.spectator = !!options.spectator; + + self.ai = !!options.ai; + self.personality = options.personality || ''; + + /* for now, only AI have landing policy. values: ['no_restriction', 'on_player_planet', 'off_player_planet'] */ + self.landingPolicy = (self.ai && options.landing_policy) ? options.landing_policy : 'no_restriction'; + + self.commander = commanders.getRandomDefaultCommanderSpec(); + + self.ready = self.ai ? true : false; + self.loading = self.ai ? false : true; + + self.armyIndex = -1; + self.slotIndex = -1; + + self.colorIndex = (self.ai || self.spectator) ? [-1, -1] : colors.takeRandomAvailableColor(); + + self.economyFactor = _.isFinite(options.economy_factor) ? options.economy_factor : 1.0; + self.economyFactor = Math.min(Math.max(0.0, self.economyFactor), 5.0); + + if (!!options.mod) + bouncer.addPlayerToModlist(client.id); + else + bouncer.removePlayerFromModlist(client.id); + + self.nextPrimaryColorIndex = function () { + if (self.spectator) + return; + + self.returnColorIndex(); + self.colorIndex = [colors.takeNextAvailableColorIndex(self.colorIndex[0]), 0]; + }; + + self.nextSecondaryColorIndex = function () { + if (self.spectator) + return; + + var colors = colors.getSecondaryColorsFor(self.colorIndex[0]); + var max = colors.getNumberOfColors(); + self.colorIndex[1] = (self.colorIndex[1] + 1) % max; + } + + self.clearColorIndex = function () { + self.returnColorIndex(); + self.colorIndex = [-1, -1]; + }; + + self.maybeTakeColorIndex = function () { + if (self.spectator || self.colorIndex[0] !== -1) + return; + self.colorIndex = colors.takeRandomAvailableColor(); + }; + + self.setPrimaryColorIndex = function (index) { + if (self.spectator) + return; + self.colorIndex = [colors.maybeGetNewColorIndex(self.colorIndex[0], index), self.colorIndex[1]]; + }; + + self.setSecondaryColorIndex = function (index) { + if (self.spectator) + return; + + self.colorIndex = [self.colorIndex[0], index]; + }; + + self.returnColorIndex = function () { + if (self.colorIndex[0] === -1) + return; + + colors.returnColorIndex(self.colorIndex[0]); + self.colorIndex = [-1, -1]; + }; + + self.adjustArmyIndexAboveTarget = function (target_index, delta) { + if (self.armyIndex > target_index) + self.armyIndex += delta; + }; + + self.adjustSlotIndexAboveTarget = function (target_index, delta) { + if (self.slotIndex > target_index) + self.slotIndex += delta; + }; + + self.processRemoveIndex = function (target_index) { + if (self.armyIndex === target_index) { + self.armyIndex = -1; + self.slotIndex = -1; + } + else + self.adjustArmyIndexAboveTarget(target_index, -1); + }; + + self.processRemoveSlotAtIndex = function (target_index, target_slot) { + if (self.armyIndex === target_index) { + + if (self.slotIndex === target_slot) { + self.armyIndex = -1; + self.slotIndex = -1; + } + else + self.adjustSlotIndexAboveTarget(target_slot, -1); + } + }; + + self.setAIPersonality = function (personality) { + if (!self.ai) + return; + + self.personality = personality; + }; + + self.setAILandingPolicy = function (policy) { + if (!self.ai) + return; + + self.landingPolicy = policy; + }; + + self.setEconomyFactor = function (value) { + value = Math.min(Math.max(0.0, value), 5.0); + self.economyFactor = value; + }; + + self.asJson = function () { + return { + name: self.client.name, + id: self.client.id, + ai: self.ai, + personality: self.personality, + landing_policy: self.landingPolicy, + economy_factor: self.economyFactor, + connected: self.ai ? true : self.client.connected, + creator: self.ai ? false : self.creator, + mod: self.ai ? false : bouncer.isPlayerMod(self.client.id), + army_index: self.armyIndex, + slot_index: self.slotIndex, + commander: self.commander, + ready: self.ready, + loading: self.loading, + color: colors.getColorFor(self.colorIndex), + color_index: self.colorIndex[0] + }; + }; + + self.finalize = function () { + return { + name: self.client.name, + commander: self.commander, + client: self.client, + army: self.armyIndex, + slot: self.slotIndex, + ai: self.ai, + personality: self.personality, + landing_policy: self.landingPolicy + }; + }; + + self.setCommander = function (new_commander) { + if (!new_commander || self.commander === new_commander) + return; + + var commanderObject = commanders.getCommanderObjectName(new_commander); + if (!commanderObject) { + debug_log("Failed to locate item " + (new_commander)); + return; + } + + // Do not validate AI commanders + + if (!self.ai && !self.client.validateItem(commanderObject)) { + debug_log("Failed to validate ownership of " + (commanderObject)); + return; + } + + self.commander = new_commander; + + lobbyModel.updatePlayerState(); + } +}; + +function ArmyModel(options) { + var self = this; + + self.slots = options.slots ? Math.max(options.slots, 1) : 1; + self.alliance = !!options.alliance; + self.allianceGroup = 0; /* 0 indicates no alliance */ + + self.asJson = function () { + return { + slots: self.slots, + alliance: self.alliance + }; + }; + + self.finalizeAsConfig = function () { + var s = []; + s.length = self.slots; + + _.forEach(s, function (element, index) { + s[index] = 'player'; + }); + + return { + slots: s, + alliance_group: self.allianceGroup + }; + }; +}; + +function LobbyModel(creator) { + var self = this; + + self.maxNumberOfAllowedPlayers = 1; + self.players = {}; + self.armies = []; + self.system = {}; + self.minimalSystemDescription = {}; /* system sans custom planet source */ + self.config = {}; + self.settings = { + hidden: true, + game_options: _.cloneDeep(DEFAULT_GAME_OPTIONS), + required_content: content_manager.getRequiredContent(), + }; /* game_mode spectators broadcast_delay private friends public tag */ + self.control = {}; /* has_first_config starting system_ready sim_ready */ + + self.creator = creator; + + self.dirty = {}; + self.allDirty = { + control: true, + system: true, + players: true, + armies: true, + color: true, + settings: true, + beacon: true + }; + // These dirty flags (on the left) will be set when the associated flags (on the right) are set + self.chainDirty = { + beacon: ['players', 'armies', 'system', 'settings'], + color: ['players'] + }; + + // Set a given flag to "dirty" (e.g. self.setDirty({control: true}); ) + self.setDirty = function(flags) { + var needsApply = _.isEmpty(self.dirty); + _.assign(self.dirty, flags); + + while (!function() { + return _.all(self.chainDirty, function(flags, key) { + if (self.dirty[key]) + return true; + var chain = _.pick(self.dirty, flags); + if (_.isEmpty(chain)) + return true; + self.dirty[key] = true; + return false; + }); + }()); + + if (needsApply) + _.delay(function() { self.cleanDirtyFlags(); }); + }; + + // Custom functions for cleaning dirty flags. (broadcast to client for all other flags) + self.customCleaners = { + beacon: function () { self.updateBeacon(); } + }; + + // Clean all the dirty flags + self.cleanDirtyFlags = function () { + try { + _.forIn(self.dirty, function(yes, key) { + if (self.customCleaners.hasOwnProperty(key)) + self.customCleaners[key](); + else { + server.broadcast({ + message_type: key, + payload: client_state[key] + }); + } + }); + } + catch (e) { + // Note: Very important that the server "mostly" works if this happens. + console.error('Lobby unable to clean dirty flags:', e.toString()); + } + self.dirty = {}; + }; + + self.isCreator = function (id) { + return id === self.creator; + }; + + self.playersInArmy = function(army_index) { + return _.filter(_.values(self.players), function (element){ + return element.armyIndex === army_index; + }); + }; + + self.aiCount = function () { + return _.filter(_.values(self.players), function (element) { return element.ai }).length; + } + + self.totalSlots = function () { + + var result = 0; + _.forEach(self.armies,function (element) { + result += element.slots; + }); + + return result; + }; + + self.totalCurrentPlayers = function () { + var total = 0; + _.forEach(self.armies, function (element, index) { + total += self.playersInArmy(index).length; + }); + return total; + }; + + self.breakArmyIntoAlliances = function (army_index) { + var army = self.armies[army_index]; + if (!army) + return; + + var extra = army.slots - 1; + army.slots = 1; + var options = army.asJson(); + + var group = alliance_groups.splice(0, 1)[0]; + + _.invoke(self.players, 'adjustArmyIndexAboveTarget', army_index, extra); + + _.times(extra, function (index) { /* split armies */ + self.armies.splice(army_index, 0, new ArmyModel(options)); /* splice modifies the array */ + }); + + _.forEach(self.playersInArmy(army_index), function (element) { /* add players to new armies */ + element.armyIndex += element.slotIndex; + element.slotIndex = 0; + + self.armies[element.armyIndex].allianceGroup = group; + }); + } + + self.finalizeConfig = function () { + return { + "armies": _.invoke(self.armies, 'finalizeAsConfig'), + "system": self.system, + "enable_lan": true, + "spectators": 0, + "password": "", + "friends": [], + "blocked": [], + "public": true, + "players": self.totalCurrentPlayers(), + "vs_ai": false, + "game_options": self.settings.game_options + }; + } + + self.finalizePlayers = function () { + var result = {}; + + _.forIn(self.players, function (value, key) { + result[key] = value.finalize() + }); + + return result; + }; + + self.finalizeArmies = function () { + var result = _.invoke(self.armies, 'finalizeAsConfig'); + + _.forIn(self.players, function (element) { + var army = result[element.armyIndex]; + if (!army) /* player is spectating */ + return; + + army.slots[element.slotIndex] = element.finalize(); /* insert finalized block { name, commander, client, army, ai } */ + + if (element.slotIndex === 0) { /* only use the color for the first player in the army */ + result[element.armyIndex].color = colors.getColorFor(element.colorIndex); /* insert expanded color */ + result[element.armyIndex].color_index = element.colorIndex[0]; + result[element.armyIndex].econ_rate = element.economyFactor; + } + + if (element.ai) { + result[element.armyIndex].personality = element.personality; + result[element.armyIndex].landing_policy = element.landingPolicy; + } + }); + + return result; + } + + self.getFinalData = function () { + + while (true) + { + var target = _.findIndex(self.armies, function (element) { + return element.alliance && element.slots > 1; + }); + + if (target === -1) + break; + + self.breakArmyIntoAlliances(target); + } + + _.invoke(self.players, 'maybeTakeColorIndex'); + + return { + game: lobbyModel.finalizeConfig(), + armies: lobbyModel.finalizeArmies(), + players: lobbyModel.finalizePlayers(), + ranked: false + } + }; + + self.updatePlayerState = function () { + debug_log('updatePlayerState'); + var players = _.invoke(self.players, 'asJson'); + if (_.isEqual(client_state.players, players)) + return; + client_state.players = _.cloneDeep(players); + self.setDirty({players: true}); + }; + + self.updateArmyState = function () { + debug_log('updateArmyState'); + var armies = _.invoke(self.armies, 'asJson'); + if (_.isEqual(client_state.armies, armies)) + return; + client_state.armies = _.cloneDeep(armies); + self.setDirty({ armies: true }); + }; + + self.updateSystemState = function () { + debug_log('updateSystemState'); + if (_.isEqual(client_state.system, self.minimalSystemDescription)) + return; + client_state.system = _.cloneDeep(self.minimalSystemDescription); + self.setDirty({system: true}); + }; + + self.changeSystem = function (system) { + if (_.isEqual(system, self.system)) + return; + + self.changeControl({ system_ready: false, sim_ready: false }); + self.system = system; + + self.minimalSystemDescription = utils.getMinimalSystemDescription(self.system); + + self.updateSystemState(); + + /* this will take some time. the server will be unresponsive. */ + sim.shutdown(false); + sim.systemName = lobbyModel.system.name; + sim.planets = self.system.planets; + }; + + self.updateColorState = function () { + debug_log('updateColorState'); + if (_.isEqual(client_state.colors, colors.colors)) + return; + client_state.colors = _.cloneDeep(colors.colors); + self.setDirty({colors: true}); + }; + + self.updateControlState = function () { + debug_log('updateControlState'); + if (_.isEqual(client_state.control, self.control)) + return; + client_state.control = _.cloneDeep(self.control); + self.setDirty({control: true}); + }; + + self.changeControl = function (control /* has_first_config starting system_ready sim_ready */) { + _.assign(self.control, control); + self.updateControlState(); + }; + + self.updateSettingsState = function () { + debug_log('updateSettingsState'); + if (_.isEqual(client_state.settings, self.settings)) + return; + client_state.settings = _.cloneDeep(self.settings); + + main.spectators = Number(self.settings.spectators); + + self.setDirty({ settings: true }); + }; + + self.changeSettings = function (settings /* game_mode spectators hidden friends public tag */) { + _.assign(self.settings, settings); + self.updateSettingsState(); + }; + + self.updateClientState = function () { + debug_log('updateClientState'); + self.updatePlayerState(); + self.updateArmyState(); + self.updateSystemState(); + self.updateColorState(); + self.updateControlState(); + self.updateSettingsState(); + }; + + self.unreadyAllPlayers = function () { + debug_log('unreadyAllPlayers'); + _.forIn(self.players, function (element) { + if (element.ready && !element.ai) { + server.broadcastEventMessage(element.client.name, ' is no longer ready.'); + element.ready = false; + } + }); + + self.setDirty({ players: true }); + self.updatePlayerState(); + }; + + self.addPlayersToSlotsIfPossible = function () { + debug_log('addPlayersToSlotsIfPossible'); + + var army_index = 0; + _.forIn(self.players, function (element, key) { + + if (element.armyIndex !== -1 || element.spectator) + return; + + while (army_index < self.armies.length) + { + if (self.addPlayerToArmy(key, army_index)) + break; + army_index += 1; + } + }); + } + + self.addArmy = function (options) { + if (self.armies.length >= MAX_PLAYERS) + return; + + if (options.slots && options.slots + self.totalSlots() > MAX_PLAYERS) + return; + + self.unreadyAllPlayers(); + + self.armies.push(new ArmyModel(options)); + self.addPlayersToSlotsIfPossible(); + self.updateArmyState(); + }; + + self.removeArmy = function (army_index) { + var spares = []; + + self.unreadyAllPlayers(); + + self.armies.splice(army_index, 1); + + _.forEach(self.players, function (element) { + if (element.ai && element.armyIndex === army_index) + spares.push(element.client.id) + }); + + _.forEach(spares, self.removePlayer); + + _.invoke(self.players, 'processRemoveIndex', army_index); + + self.addPlayersToSlotsIfPossible(); + self.updateArmyState(); + self.updatePlayerState(); + }; + + self.modifyArmy = function (army_index, options) { + debug_log('modifyArmy'); + + var spares = []; + + var army = self.armies[army_index]; + if (!army) + return; + + var new_options = _.assign(army.asJson(), options); + + var ai = !!_.filter(self.playersInArmy(army_index), function (element) { return element.ai }).length; + if (ai) + new_options.alliance = true; /* override to prevent shared army with ai */ + + if (options.slots && (options.slots - army.slots + self.totalSlots()) > MAX_PLAYERS) + return; + + self.unreadyAllPlayers(); + + if (options.slots < army.slots) { + _.forEach(_.range(options.slots, army.slots), function (index) { + _.invoke(self.players, 'processRemoveSlotAtIndex', army_index, index); + + _.forEach(self.players, function (element) { + if (element.ai && element.armyIndex === army_index && element.slotIndex == index) + spares.push(element.client.id) + }); + }); + } + + _.forEach(spares, self.removePlayer); + + self.armies[army_index] = new ArmyModel(new_options); + + self.addPlayersToSlotsIfPossible(); + + self.fixColors(); + + self.updateArmyState(); + self.updateColorState(); + self.updatePlayerState(); + }; + + self.numPlayerSlots = function() { + return utils.sum(self.armies, function(army) { return army.ai ? 0 : army.slots; }); + }; + + self.numPlayers = function () { + return _.keys(self.players).length; + }; + + self.updateBeacon = function () { + debug_log('updateBeacon'); + var numPlayerSlots = self.numPlayerSlots(); + server.maxClients = Math.min(MAX_PLAYERS, numPlayerSlots) + Math.min(MAX_SPECTATORS, main.spectators); + var publish = !server.closed && (self.settings.public || bouncer.getWhitelist().length) && !self.settings.hidden; + + if (publish) { + var full = server.clients.length >= server.maxClients; + + var modNames = []; + var mods = server.getMods(); + if (mods !== undefined && mods.mounted_mods !== undefined) { + _.forEach(mods.mounted_mods, function (element) { + modNames.push(element.display_name); + }); + } + + var player_names = _.map(_.filter(self.players, { 'spectator': false }), function (player) { return player.client.name; }); + var spectator_names = _.map(_.filter(self.players, { 'spectator': true }), function (player) { return player.client.name; }); + + var mode = DEFAULT_GAME_TYPE; + if (self.settings.game_options) + mode = self.settings.game_options.game_type; + + server.beacon = { + uuid: server.uuid(), + full: full, + started: self.control.countdown || self.control.starting, + players: player_names.length, + creator: self.creatorName(), + max_players: numPlayerSlots, + spectators: spectator_names.length, + max_spectators: main.spectators, + mode: mode, + mod_names: modNames, + cheat_config: main.cheats, + player_names: player_names, + spectator_names: spectator_names, + require_password: !!bouncer.doesGameRequirePassword(), + whitelist: bouncer.getWhitelist(), + blacklist: bouncer.getBlacklist(), + tag: self.settings.tag, + game: { + system: self.minimalSystemDescription, + name: self.settings.game_name + }, + required_content: content_manager.getRequiredContent(), + bounty_mode: self.settings.game_options ? !!self.settings.game_options.bounty_mode : false, + bounty_value: self.settings.game_options ? self.settings.game_options.bounty_value : 0.5, + sandbox: self.settings.game_options ? !!self.settings.game_options.sandbox : false + }; + } else { + server.beacon = null; + } + }; + + self.addPlayer = function (client, options) { + debug_log('addPlayer'); + var player = new PlayerModel(client, options); + self.players[client.id] = player; + self.updatePlayerState(); + self.updateColorState(); + + if (!options.ai) { + _.delay(server.broadcastEventMessage, 500, player.client.name, ' joined the lobby.'); + + debug_log('Player Stats: '); + debug_log(player.client.statistics); + client.incrementStatistic("TestStat_LobbiesJoined", 1); + } + }; + + self.addAI = function (payload) { + var player_id = getAIId(); + var success = false; + + if (!payload) + return; + + self.addPlayer({ connected: true, id: player_id, name: getAIName() }, payload.options); + + success = self.addPlayerToArmy(player_id, payload.army_index); + if (!success) + self.removePlayer(player_id); + }; + + self.creatorName = function () { + var player = self.players[self.creator]; + return player ? player.client.name : ''; + }; + + self.chooseNextPlayerAsCreator = function () { + debug_log('chooseNextPlayerAsCreator'); + if (_.isEmpty(self.players)) + return; + + var client = _.first(server.clients); + if (!client) + return; + + var id = client.id; + self.creator = id; + + var player = self.players[id]; + + player.creator = true; + bouncer.addPlayerToModlist(id); + + self.updatePlayerState(); + server.broadcastEventMessage(player.client.name, ' is now the host.'); + }; + + self.removePlayer = function (id) { + debug_log('removePlayer'); + + delete client_state.players[id]; + var player = self.players[id]; + + if (player) { + self.removePlayerFromArmy(id, { clear_color: true }); + + var ai = player.ai; + if (!ai) + server.broadcastEventMessage(player.client.name, ' has left the lobby.'); + + player.returnColorIndex(); + delete self.players[id]; + + if (!ai) { + console.log('killing client ' + id); + player.client.kill(); + + // Terminate empty lobbies + if (!main.keep_alive && !server.connected) { + sim.shutdown(false); + server.exit(); + return; + } + + if (id === self.creator) + self.chooseNextPlayerAsCreator(); + } + + if (ai) + returnAIId(id); + + self.updatePlayerState(); + self.updateColorState(); + } + }; + + self.kickPlayer = function (id) { + debug_log('kickPlayer'); + bouncer.addPlayerToBlacklist(id); + self.removePlayer(id); + }; + + self.addPlayerToArmy = function (player_id, army_index) { + debug_log('addPlayerToArmy'); + var player = self.players[player_id]; + var army = self.armies[army_index]; + + var array = self.playersInArmy(army_index); + var count = array.length; + + if (!player || !army || count >= army.slots) + return false; + + if (player.armyIndex === army_index) + return false; + + player.armyIndex = army_index; + player.slotIndex = count; + player.spectator = false; + + if (player.ai) + army.alliance = true; /* override to prevent shared army with ai */ + + self.fixColors(); + + self.updatePlayerState(); + self.updateArmyState(); + self.updateColorState(); + return true; + }; + + self.removePlayerFromArmy = function (player_id, options) { + debug_log('removePlayerFromArmy'); + var player = self.players[player_id]; + + if (!player) + return false; + + if (options && options.clear_color) + player.clearColorIndex(); + + var army_index = player.armyIndex; + var slot_index = player.slotIndex; + + player.armyIndex = -1; + player.slotIndex = -1; + + if (options && options.set_spectator) + player.spectator = true; + + /* move players down to fill empty slot */ + _.invoke(self.players, 'processRemoveSlotAtIndex', army_index, slot_index); + + self.fixColors(); + + self.updatePlayerState(); + self.updateArmyState(); + self.updateColorState(); + + return true; + } + + /* this checks every army, which is excessive; however, it is very reliable. */ + self.fixColors = function () { + _.forEach(self.armies, function (element, index) { + self.fixColorsForArmy(index); + }); + } + + /* it would be preferrable to just call this function for each modified army */ + self.fixColorsForArmy = function (army_index) { + + var army = self.armies[army_index]; + + if (!army) + return; + + _.forEach(self.playersInArmy(army_index), function (element, index) { + if (index === 0 || army.alliance) + element.maybeTakeColorIndex(); + else + element.returnColorIndex(); /* only the first player in a shared army gets a color */ + }); + + self.setDirty({ colors: true }); + }; + + self.setPrimaryColorIndex = function (player_id, index, ai) { + debug_log('{{setPrimaryColorIndex}} ' + index); + var player = self.players[player_id]; + + if (!player || player.ai !== ai || !colors.isValidPrimaryColorIndex(index)) + return; + + player.setPrimaryColorIndex(index); + + self.updatePlayerState(); + self.updateColorState(); + }; + + self.setSecondaryColorIndex = function (player_id, index, ai) { + debug_log('{{setSecondaryColorIndex}} ' + index); + var player = self.players[player_id]; + + if (!player || player.ai !== ai || !colors.isValidColorPair(player.colorIndex[0], index)) + return; + + player.setSecondaryColorIndex(index); + + self.updatePlayerState(); + self.updateColorState(); + }; + + self.setAIPersonality = function (player_id, personality) { + debug_log('setAIPersonality'); + var player = self.players[player_id]; + + if (!player || !player.ai) + return; + + player.setAIPersonality(personality); + + self.unreadyAllPlayers(); + self.updatePlayerState(); + }; + + self.setAILandingPolicy = function (player_id, policy) { + debug_log('setAILandingPolicy'); + var player = self.players[player_id]; + + if (!player || !player.ai) + return; + + player.setAILandingPolicy(policy); + + self.unreadyAllPlayers(); + self.updatePlayerState(); + }; + + self.setAICommander = function (player_id, commander) { + var player = self.players[player_id]; + + if (!player || !player.ai) + return; + + player.setCommander(commander); + + self.unreadyAllPlayers(); + self.updatePlayerState(); + }; + + self.setEconomyFactor = function (player_id, value) { + debug_log('setEconomyFactor'); + var player = self.players[player_id]; + + if (!player) + return; + + if (player.economyFactor != value) + { + player.setEconomyFactor(value); + + self.unreadyAllPlayers(); + self.updatePlayerState(); + } + }; + + self.validateSetup = function () { + debug_log('validateSetup'); + var totalOpenSlots = self.numPlayerSlots(); + var totalPlayersInSlots = utils.sum(self.players, function (player) { return player.armyIndex >= 0; }); + + debug_log('Validation:'+ totalPlayersInSlots+ '/'+ totalOpenSlots); /* todo: add validation stage to control state */ + + if (totalPlayersInSlots !== totalOpenSlots) + return 'Empty slots encountered'; + }; + + self.gameType = function() { + if (!self.settings.game_options) + return null; + return self.settings.game_options.game_type; + }; +}; + +var lobbyModel; + +var cleanup = []; +var cleanupOnEntry = []; + +function allowChangesFrom(client) { + if (!client || !lobbyModel.isCreator(client.id)) + return false; + if (lobbyModel.control.countdown) + return false; + if (lobbyModel.control.starting) + return false; + + return true; +} + +function playerMsg_resetArmies(msg){ + debug_log('playerMsg_resetArmies'); + var response = server.respond(msg); + var payload = msg.payload; + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + _.forEach(lobbyModel.players, function (element, key) { + if (element.ai) + lobbyModel.removePlayer(key); + else + lobbyModel.removePlayerFromArmy(key); + }); + + lobbyModel.armies = []; + _.forEach(payload, function (element) { + lobbyModel.addArmy(element); + }); + + lobbyModel.changeControl({ has_first_config: true }); + + response.succeed(); +}; + +function playerMsg_addArmy(msg /* client payload */){ + debug_log('playerMsg_addArmy'); + var response = server.respond(msg); + var payload = msg.payload; + + if (!payload.options) + return response.fail("Not allowed."); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.addArmy(payload.options); + + response.succeed(); +} + +function playerMsg_removeArmy(msg){ + debug_log('playerMsg_removeArmy'); + var response = server.respond(msg); + var payload = msg.payload; + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.removeArmy(payload.army_index); + + response.succeed(); +} + +function playerMsg_addAI(msg) { + debug_log('playerMsg_addAI'); + + var response = server.respond(msg); + var payload = msg.payload; + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.addAI(payload); + + response.succeed(); +} + +function playerMsg_modifySystem(msg) { + debug_log('modifySystem'); + var response = server.respond(msg); + var payload = msg.payload; + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + var systemValidationResult = sim_utils.validateSystemConfig(msg.payload); + if (_.isString(systemValidationResult)) { + debug_log('Invalid system'); + return response.fail("Invalid system provided - " + systemValidationResult); + } + else + systemValidationResult.then( function () { + lobbyModel.changeSystem(msg.payload); + response.succeed(); + }); +} + +function playerMsg_modifyArmy(msg){ + debug_log('modifyArmy'); + var response = server.respond(msg); + var payload = msg.payload; + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.modifyArmy(payload.army_index, payload.options); + + response.succeed(); +} + +function updateBouncer(config) { + + if (config.password || bouncer.doesGameRequirePassword()) + bouncer.setPassword(config.password); + + bouncer.clearWhitelist(); + var hasFriendsList = config.friends && config.friends.length; + if (hasFriendsList) { + _.forEach(config.friends, function (element) { bouncer.addPlayerToWhitelist(element); }); + } + + bouncer.clearBlacklist(); + var hasBlockList = config.blocked && config.blocked.length; + if (hasBlockList) { + _.forEach(config.blocked, function (element) { bouncer.addPlayerToBlacklist(element); }); + } +} + + +function playerMsg_modifyBouncer(msg) { + debug_log('playerMsg_modifyBouncer'); + + var config = msg.payload; + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + updateBouncer(config); + + lobbyModel.setDirty({ beacon: true }); + + response.succeed(); +} + +function playerMsg_modifySettings(msg) { + + debug_log('playerMsg_modifySettings'); + var config = msg.payload; + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) { + lobbyModel.setDirty({ settings: true }); + return response.fail("Not allowed."); + } + + var game_options = _.cloneDeep(DEFAULT_GAME_OPTIONS); + + var settings = { + max_spectators: MAX_SPECTATORS, + max_players: MAX_PLAYERS, + spectators: MAX_SPECTATORS, + hidden: false, + friends: false, + public: true, + tag: DEFAULT_LOBBY_TAG, + game_name: DEFAULT_LOBBY_NAME, + required_content: content_manager.getRequiredContent(), + }; + + if (config.game_options) + game_options.game_type = config.game_options.game_type; + else if (client_state.settings && client_state.settings.game_options) + game_options.game_type = client_state.settings.game_options.game_type; + + if (!isValidGameType(game_options.game_type)) + game_options.game_type = DEFAULT_GAME_TYPE; + + updateBouncer(config); + var hasFriendsList = bouncer.getWhitelist().length; + + if (config.game_options) + { + game_options.land_anywhere = !!config.game_options.land_anywhere; + + if (isFFAType(game_options.game_type)) { + game_options.dynamic_alliances = !!config.game_options.dynamic_alliances; + if (game_options.dynamic_alliances) + game_options.dynamic_alliance_victory = !!config.game_options.dynamic_alliance_victory; + } + if (config.game_options.bounty_mode) + game_options.bounty_mode = _.contains(content_manager.getRequiredContent(), 'PAExpansion1') && !!config.game_options.bounty_mode; + if (config.game_options.bounty_value) + game_options.bounty_value = config.game_options.bounty_value; + if (config.game_options.sandbox) + game_options.sandbox = !!config.game_options.sandbox; + if (config.game_options.listen_to_spectators) + game_options.listen_to_spectators = !!config.game_options.listen_to_spectators; + } + + settings.hidden = (!hasFriendsList && !config.public); + settings.friends = !!hasFriendsList; + settings.public = (config.public && !hasFriendsList); + if (config.tag) + settings.tag = config.tag; + + settings.spectators = Math.min(Number(config.spectators), MAX_SPECTATORS); + + if (_.isString(config.game_name)) + settings.game_name = config.game_name.substring(0, Math.min(config.game_name.length, 128)); + + _.forEach(_.keys(lobbyModel.settings.game_options), function (key) { + if (lobbyModel.settings.game_options[key] !== game_options[key]) { + var name = key.replace(/_/g, ' '); + var message = name + ' ' + + (_.isBoolean(game_options[key]) + ? (!!game_options[key] ? 'enabled' : 'disabled') + : ('changed')) + + '.'; + server.broadcastEventMessage('', message, 'settings'); + } + }); + + settings.game_options = game_options; + + var nameChangeOnly = false; + + if (settings.game_name !== lobbyModel.settings.game_name) { + var currentSettings = _.cloneDeep(lobbyModel.settings); + delete currentSettings.game_name; + var newSettings = _.cloneDeep(settings); + delete newSettings.game_name; + + nameChangeOnly = _.isEqual(currentSettings, newSettings); + } + + lobbyModel.changeSettings(settings); + + if (!nameChangeOnly) { + lobbyModel.unreadyAllPlayers(); + } + + response.succeed(); +} + +function maybeStartLandingState() { + debug_log('maybeStartLandingState'); + if (!lobbyModel.control.starting || !lobbyModel.control.system_ready || !lobbyModel.control.sim_ready) + return; + + lobbyModel.updateClientState(); + + var final_data = lobbyModel.getFinalData(); + + try { + if (server_utils.log_lobby_description) { + console.log('final lobby data:'); + console.log(JSON.stringify(final_data, null, '\t')); + } + } + catch (e) { + console.log('final lobby data: failed.'); // this is *not* expected. + }; + + hasStartedPlaying = true; + main.setState(main.states.landing, final_data); +} + + + +function playerMsg_startCountdown(msg) { + debug_log('playerMsg_startGame'); + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + var player = lobbyModel.players[msg.client.id]; + player.ready = true; + + function not_ready (){ + return _.some(lobbyModel.players, function (value) { + return !value.ready && !value.spectator; + }); + } + + if (not_ready()) { + player.ready = false; + return response.fail("Not ready."); + + } + + var lobbyValidationResult = lobbyModel.validateSetup(); + if (lobbyValidationResult) { + player.ready = false; + return response.fail("Invalid game setup - " + lobbyValidationResult); + } + + if (!lobbyModel.control.sim_ready) { + player.ready = false; + return response.fail("Server is not done gerating planets"); + } + + lobbyModel.updatePlayerState(); + lobbyModel.changeControl({ countdown: true }); + + function startGame() { + + server.broadcastEventMessage('', -1, 'countdown'); + + server.broadcastEventMessage('', 'Game is starting.'); + + lobbyModel.changeControl({ starting: true }); + maybeStartLandingState(); + } + + var count = _.has(msg, 'countdown') ? msg.countdown : START_GAME_DELAY; + function countdownToStartGame() { + server.broadcastEventMessage('', count, 'countdown'); + count -= 1; + + if (count > 0) + setTimeout(countdownToStartGame, 1000); + else + setTimeout(startGame, 1000); + } + + if (lobbyModel.totalCurrentPlayers() < 2) + startGame(); + else { + server.broadcastEventMessage('', 'Game will start in ' + START_GAME_DELAY + ' seconds.'); + countdownToStartGame(); + } + + response.succeed(); +} + +function playerMsg_setPrimaryColorIndex(msg) { + debug_log('playerMsg_setPrimaryColorIndex'); + debug_log(msg); + var response = server.respond(msg); + + lobbyModel.setPrimaryColorIndex(msg.client.id, msg.payload, false); + response.succeed(); +} + +function playerMsg_setPrimaryColorIndexForAI(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setPrimaryColorIndex(msg.payload.id, msg.payload.color, true); + response.succeed(); +} + +function playerMsg_setSecondaryColorIndex(msg) { + debug_log('playerMsg_setSecondaryColorIndex'); + debug_log(msg); + var response = server.respond(msg); + + lobbyModel.setSecondaryColorIndex(msg.client.id, msg.payload, false); + response.succeed(); +} + +function playerMsg_setSecondaryColorIndexForAI(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setSecondaryColorIndex(msg.payload.id, msg.payload.color, true); + response.succeed(); +} + +function playerMsg_setAIPersonality(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setAIPersonality(msg.payload.id, msg.payload.ai_personality); + response.succeed(); +} + +function playerMsg_setAILandingPolicy(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setAILandingPolicy(msg.payload.id, msg.payload.ai_landing_policy); + response.succeed(); +} + +function playerMsg_setAICommander(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setAICommander(msg.payload.id, msg.payload.ai_commander); + + response.succeed(); +} + +function playerMsg_setEconomyFactor(msg) { + var response = server.respond(msg); + + if (!allowChangesFrom(msg.client)) + return response.fail("Not allowed."); + + lobbyModel.setEconomyFactor(msg.payload.id, msg.payload.economy_factor); + response.succeed(); +} + +function playerMsg_updateCommander(msg) { + debug_log('playerMsg_updateCommander'); + var response = server.respond(msg); + var player = lobbyModel.players[msg.client.id]; + + if (!msg.payload || !msg.payload.commander || !player) + return response.fail("Invalid message"); + + player.setCommander(msg.payload.commander); + + return response.succeed(); +} + +function playerMsg_joinArmy(msg) { + debug_log('playerMsg_joinArmy'); + var response = server.respond(msg); + var player = lobbyModel.players[msg.client.id]; + + if (!msg.payload || !player) + return response.fail("Invalid message"); + + if (msg.payload.commander) + player.setCommander(msg.payload.commander); + + lobbyModel.removePlayerFromArmy(msg.client.id); + if (lobbyModel.addPlayerToArmy(msg.client.id, msg.payload.army)) { + server.broadcastEventMessage(player.client.name, ' has joined an army.'); + response.succeed(); + } else { + response.fail("Unable to add player to army"); + } +} + +function playerMsg_toggleReady(msg) { + debug_log('playerMsg_toggleReady'); + + var response = server.respond(msg); + var player = lobbyModel.players[msg.client.id]; + + if (!player) + return response.fail("Invalid message"); + + if (client_state.control.countdown) + return response.fail("Cannot change ready after countdown has started."); + + player.ready = !player.ready; + + lobbyModel.updatePlayerState(); + + server.broadcastEventMessage(player.client.name, player.ready ? ' is now ready.' : ' is no longer ready.'); + + response.succeed(); +} + +function playerMsg_leaveArmy(msg) { + debug_log('playerMsg_leaveArmy'); + var response = server.respond(msg); + + var player = lobbyModel.players[msg.client.id]; + if (!player) + return; + + if (lobbyModel.removePlayerFromArmy(msg.client.id, { clear_color: true, set_spectator: true })) { + server.broadcastEventMessage(player.client.name, ' is now a spectator.'); + response.succeed(); + } else { + response.fail('Could not remove you from the army'); + } +} + +function playerMsg_chatMessage(msg) { + debug_log('playerMsg_chatMessage'); + var response = server.respond(msg); + if (!msg.payload || !msg.payload.message) + return response.fail("Invalid message"); + server.broadcast({ + message_type: 'chat_message', + payload: { + player_name: msg.client.name, + message: msg.payload.message + } + }); + response.succeed(); +} + +function playerMsg_leave(msg) { + debug_log('playerMsg_leave'); + var response = server.respond(msg); + + lobbyModel.removePlayer(msg.client.id); + + response.succeed(); +} + +function playerMsg_kick(msg) { + debug_log('playerMsg_kick'); + debug_log(msg); + var response = server.respond(msg); + + var id = msg.payload.id; + var player = lobbyModel.players[id]; + + if (!bouncer.isPlayerMod(msg.client.id)) + return response.fail("Only mods can kick."); + + if (bouncer.isPlayerMod(id)) + return response.fail("Mods cannot be kicked."); + + if (!player) + return response.fail("Already left"); + + bouncer.addPlayerToBlacklist(id); + + lobbyModel.kickPlayer(id); + + response.succeed(); +} + +function playerMsg_promoteToMod(msg) { + debug_log('playerMsg_promoteToMod'); + var response = server.respond(msg); + var id = msg.payload.id; + var player = lobbyModel.players[id]; + + if (!bouncer.isPlayerMod(msg.client.id)) + return response.fail("Only mods can promote."); + + if (!player) + return response.fail("Player is absent"); + response.succeed(); + + bouncer.addPlayerToModlist(id); + lobbyModel.updatePlayerState(); +} + +function playerMsg_setLoading(msg) { + debug_log('playerMsg_setLoading'); + + var response = server.respond(msg); + var player = lobbyModel.players[msg.client.id]; + + if (!player) + return response.fail("Invalid message"); + + player.loading = msg.payload.loading; + + lobbyModel.updatePlayerState(); + + response.succeed(); +} + +function playerMsg_modDataAvailable(msg) { + debug_log('playerMsg_modDataAvailable'); + + var response = server.respond(msg); + + if (lobbyModel.creator !== msg.client.id) { + return response.fail("Mod data can only be provided by lobby creator"); + } + + var auth_token = ""; + var hasMods = false; + var mods = server.getMods(); + if (mods !== undefined) { + auth_token = mods.auth_token || ""; + if (mods.mounted_mods !== undefined && mods.mounted_mods.length > 0) { + hasMods = true; + } + } + if (hasMods) { + return response.fail("Mod data is already mounted"); + } + + response.succeed({ "auth_token": auth_token }); +} + +function check_cheat(cheatname, callback) { + file.load('/server-script/modroot/cheat_' + cheatname + '.json', function (data) { + var cheat_enabled = false; + if (data !== undefined && data.length > 0) { + var config = JSON.parse(data); + if (config.cheat_flags !== undefined) { + if (config.cheat_flags[cheatname]) { + main.cheats.cheat_flags[cheatname] = true; + main.cheats.cheat_flags.any_enabled = true; + main.cheats.cheat_flags.cheat_mod_enabled = true; + cheat_enabled = true; + console.log('CHEATS: Mod enabled cheat: ' + cheatname); + + server.broadcast({ + message_type: 'set_cheat_config', + payload: main.cheats + }); + } + } + } + if (callback !== undefined) { + callback(cheat_enabled); + } + }); +} + +function playerMsg_modDataUpdated(msg) { + check_cheat('allow_change_vision'); + check_cheat('allow_change_control'); + check_cheat('allow_create_unit'); + check_cheat('allow_mod_data_updates', function (cheat_enabled) { + if (cheat_enabled) { + server.disableWriteReplay(); + } else { + server.resetModUpdateAuthToken(); + } + }); + + _.forEach(server.clients, function (client) { + if (client.id !== msg.client.id) { + client.downloadModsFromServer(); + } else { + client.message({ + message_type: 'mount_mod_file_data', + payload: {} + }); + } + }); +} + +function playerMsg_requestCheatConfig(msg) { + debug_log('playerMsg_requestCheatConfig'); + + var response = server.respond(msg); + + response.succeed({ "cheat_config": main.cheats }); +} + +function initOwner(owner) { + debug_log('initOwner'); + server.maxClients = 1; + + if (!owner) { + var testConfig = _.cloneDeep(require('test').exampleConfig); + main.setState(main.states.lobby, testConfig); + return; + } + + bouncer.addPlayerToModlist(owner.id); + + var client_data = { uberid: '', password: '', uuid: '' }; + try { + client_data = JSON.parse(owner.data); + bouncer.setUUID(client_data.uuid); + } + catch (error) { + debug_log('js initOwner : unable to parse owner data'); + } +} + +exports.url = 'coui://ui/main/game/new_game/new_game.html'; +exports.enter = function (owner) { + + _.forEachRight(cleanupOnEntry, function (c) { c(); }); + + lobbyModel = new LobbyModel(); + cleanupOnEntry.push(function () { lobbyModel = undefined; }); + + initOwner(owner); + lobbyModel.changeControl({ has_first_config: false, countdown: false, starting: false, system_ready: false, sim_ready: false }); + + utils.pushCallback(sim.planets, 'onReady', function (onReady) { + debug_log('sim.planets.onReady'); + lobbyModel.changeControl({ system_ready: true }); + sim.create(); + maybeStartLandingState(); + return onReady; + }); + cleanup.push(function () { sim.planets.onReady.pop(); }); + + utils.pushCallback(sim, 'onReady', function (onReady) { + debug_log('sim.onReady'); + lobbyModel.changeControl({ sim_ready: true }); + maybeStartLandingState(); + return onReady; + }); + cleanup.push(function () { sim.onReady.pop(); }); + + utils.pushCallback(server, 'onConnect', function (onConnect, client, reconnect) { + debug_log('onConnect'); + var client_data = { uberid: '', password: '', uuid: '' }; + try { + client_data = JSON.parse(client.data); + debug_log(client); + } + catch (e) { + debug_log('js utils.pushCallback : unable to parse client.data'); + server.rejectClient(client, 'Bad Client data'); + return onConnect; + } + + if (!bouncer.isPlayerValid(client_data.uberid, client_data.password, client_data.uuid, lobbyModel.settings.public)) { + console.log('invalid credentials'); + server.rejectClient(client, 'Credentials are invalid'); + return onConnect; + } + + if (!reconnect) { + var max = Math.min(MAX_CLIENTS, lobbyModel.numPlayerSlots() + main.spectators); + if (lobbyModel.numPlayers() >= max) { + console.error('lobby at capacity. client rejected.'); + server.rejectClient(client, 'No room'); + return onConnect; + } + } + + utils.pushCallback(client, 'onDisconnect', function (onDisconnect) { + if (!hasStartedPlaying) { /* don't kill the client unless we have not started any other states. */ + console.log('removing disconnected player from the lobby.'); + lobbyModel.removePlayer(client.id); + } + return onDisconnect; + }); + cleanup.push(function () { + if (client.onDisconnect) { + console.log('remove disconnect handler'); + client.onDisconnect.pop(); + } + /* removePlayer calls client.kill(), which will destroy the onDisconnect handler */ + }); + + var players = _.filter(lobbyModel.players, { 'spectator': false }); + var options = { mod: bouncer.isPlayerMod(client.id), creator: client.id === lobbyModel.creator }; + /* force the player to be a spectator */ + if (players.length >= MAX_PLAYERS) + options.spectator = true; + + if (!lobbyModel.players.hasOwnProperty(client.id)) + lobbyModel.addPlayer(client, options); + else + lobbyModel.updatePlayerState(); + + lobbyModel.addPlayersToSlotsIfPossible(); + + var player = lobbyModel.players[client.id]; + if (player.armyIndex === -1) /* make the player a spectator if there is no room */ + player.spectator = true; + + debug_log('calling client.downloadModsFromServer'); + client.downloadModsFromServer(); + + client.message({ + message_type: 'set_cheat_config', + payload: main.cheats + }); + + return onConnect; + }); + cleanupOnEntry.push(function () { server.onConnect.pop(); }); + + lobbyModel.creator = owner.id; + lobbyModel.addPlayer(owner, { mod: true, creator: true }); + bouncer.addPlayerToModlist(owner.id); + + _.forEach(server.clients, function (client) { + if (client !== owner) { + lobbyModel.addPlayer(client, { mod: false, creator: false }); + } + }); + + var removeHandlers = server.setHandlers({ + reset_armies: playerMsg_resetArmies, + add_army: playerMsg_addArmy, + remove_army: playerMsg_removeArmy, + add_ai: playerMsg_addAI, + modify_system: playerMsg_modifySystem, + modify_army: playerMsg_modifyArmy, + modify_bouncer: playerMsg_modifyBouncer, + modify_settings: playerMsg_modifySettings, + start_game: playerMsg_startCountdown, + set_primary_color_index: playerMsg_setPrimaryColorIndex, + set_primary_color_index_for_ai: playerMsg_setPrimaryColorIndexForAI, + set_secondary_color_index: playerMsg_setSecondaryColorIndex, + set_secondary_color_index_for_ai: playerMsg_setSecondaryColorIndexForAI, + set_ai_personality: playerMsg_setAIPersonality, + set_ai_landing_policy: playerMsg_setAILandingPolicy, + set_ai_commander: playerMsg_setAICommander, + set_econ_factor: playerMsg_setEconomyFactor, + join_army: playerMsg_joinArmy, + toggle_ready: playerMsg_toggleReady, + leave_army: playerMsg_leaveArmy, + update_commander: playerMsg_updateCommander, + chat_message: playerMsg_chatMessage, + leave: playerMsg_leave, + kick: playerMsg_kick, + promote_to_mod: playerMsg_promoteToMod, + set_loading: playerMsg_setLoading, + mod_data_available: playerMsg_modDataAvailable, + mod_data_updated: playerMsg_modDataUpdated, + request_cheat_config: playerMsg_requestCheatConfig + }); + cleanup.push(function () { removeHandlers(); }); + + lobbyModel.updateBeacon(); + + return client_state; +}; + +exports.exit = function (newState) { + _.forEachRight(cleanup, function (c) { c(); }); + cleanup = []; + + return true; +}; + +main.gameModes.lobby = exports; +// This is for backwards compatibility. Game used to ask for 'Config' game mode. +main.gameModes.Config = exports; diff -Naur server-script.ori/utils.js server-script/utils.js --- server-script.ori/utils.js 2016-02-07 14:27:12.000000000 +0100 +++ server-script/utils.js 2016-02-06 20:38:00.182512814 +0100 @@ -1,68 +1,73 @@ -var _ = require('thirdparty/lodash'); - -// TODO: 12/16/2014 - This module requires using an exports object. Adding -// extra members in this module without using a new object exposes some bug in -// the scripting environment implementation, and nothing will get exported. -// We don't have time to figure out why right now. -exports = { - pushCallback: function(obj, name, fn) { - var oldCallback = obj[name]; - var wrapper = function() { - var oldResult = oldCallback ? oldCallback.apply(this, arguments) : undefined; - var args = [oldResult]; - for (var a = 0; a < arguments.length; ++a) - args.push(arguments[a]); - return fn.apply(this, args); - }; - wrapper.pop = function() { - obj[name] = oldCallback; - }; - obj[name] = wrapper; - }, - - // Convenience wrapper for _.reduce. Counts callbacks that return "true", or - // sums a number per item in the collection. - sum: function(collection, callback) { - return _.reduce(collection, function(sum, item) { - var itemResult = callback(item); - var itemCount; - if (typeof itemResult === 'number') - itemCount = itemResult; - else - itemCount = (itemResult ? 1 : 0); - return sum + itemCount; - }, 0); - }, - - getMinimalSystemDescription: function (system) { - if (!system) - return system; - - var copy = _.omit(system, 'planets'); - copy.planets = _.map(system.planets, function (element) { - return _.omit(element, ['planetCSG', 'landing_zones', 'metal_spots', 'source']); - }); - - return copy; - }, - - // Modulo division, which is not what % does in JS. - modulo: function(n, m) { return ((n % m) + m) % m; } -}; - -var random_characters = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'; - -var randomString = function(length) { - var result = ''; - _.times(length, function () { - var number = Math.floor(Math.random() * random_characters.length); - result += random_characters.charAt(number); - }); - return result; -} - -exports.randomString = randomString; - -exports.createUUIDString = function() { - return randomString(32); +var _ = require('thirdparty/lodash'); + +// TODO: 12/16/2014 - This module requires using an exports object. Adding +// extra members in this module without using a new object exposes some bug in +// the scripting environment implementation, and nothing will get exported. +// We don't have time to figure out why right now. +exports = { + pushCallback: function(obj, name, fn) { + var oldCallback = obj[name]; + var wrapper = function() { + var oldResult = oldCallback ? oldCallback.apply(this, arguments) : undefined; + var args = [oldResult]; + for (var a = 0; a < arguments.length; ++a) + args.push(arguments[a]); + return fn.apply(this, args); + }; + wrapper.pop = function() { + obj[name] = oldCallback; + }; + obj[name] = wrapper; + }, + + // Convenience wrapper for _.reduce. Counts callbacks that return "true", or + // sums a number per item in the collection. + sum: function(collection, callback) { + return _.reduce(collection, function(sum, item) { + var itemResult = callback(item); + var itemCount; + if (typeof itemResult === 'number') + itemCount = itemResult; + else + itemCount = (itemResult ? 1 : 0); + return sum + itemCount; + }, 0); + }, + + getMinimalSystemDescription: function (system) { + if (!system) + return system; + + var copy = _.omit(system, 'planets'); + copy.planets = _.map(system.planets, function (element) { + var summary = _.omit(element, ['planetCSG', 'landing_zones', 'metal_spots', 'source']); + + summary.metal_spots_count = element.metal_spots ? element.metal_spots.length : 0; + summary.planetCSG_count = element.planetCSG ? element.planetCSG.length : 0; + summary.landing_zones_count = element.landing_zones ? ( element.landing_zones.list ? element.landing_zones.list.length : element.landing_zones.length ) : 0; + return summary; + }); + + return copy; + }, + + // Modulo division, which is not what % does in JS. + modulo: function(n, m) { return ((n % m) + m) % m; } +}; + +var random_characters = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'; + +var randomString = function(length) { + var result = ''; + _.times(length, function () { + var number = Math.floor(Math.random() * random_characters.length); + result += random_characters.charAt(number); + }); + return result; +} + +exports.randomString = randomString; + +exports.createUUIDString = function() { + return randomString(32); } \ No newline at end of file