Index: ps/trunk/binaries/data/mods/public/globalscripts/Resources.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 20100) @@ -1,98 +1,103 @@ /** * Since the AI context can't access JSON functions, it gets passed an object * containing the information from `GuiInterface.js::GetSimulationState()`. */ function Resources() { let jsonFiles = []; // Simulation context if (Engine.FindJSONFiles) { jsonFiles = Engine.FindJSONFiles("resources", false); for (let file in jsonFiles) jsonFiles[file] = "resources/" + jsonFiles[file] + ".json"; } // GUI context else if (Engine.BuildDirEntList) jsonFiles = Engine.BuildDirEntList("simulation/data/resources/", "*.json", false); else { error("Resources: JSON functions are not available"); return; } this.resourceData = []; this.resourceDataObj = {}; this.resourceCodes = []; this.resourceNames = {}; for (let filename of jsonFiles) { let data = Engine.ReadJSONFile(filename); if (!data) continue; if (data.code != data.code.toLowerCase()) warn("Resource codes should use lower case: " + data.code); // Treasures are supported for every specified resource if (data.code == "treasure") { error("Encountered resource with reserved keyword: " + data.code); continue; } this.resourceData.push(data); this.resourceDataObj[data.code] = data; this.resourceCodes.push(data.code); this.resourceNames[data.code] = data.name; for (let subres in data.subtypes) this.resourceNames[subres] = data.subtypes[subres]; } // Sort arrays by specified order let resSort = (a, b) => a.order < b.order ? -1 : a.order > b.order ? +1 : 0; this.resourceData.sort(resSort); this.resourceCodes.sort((a, b) => resSort( this.resourceData.find(resource => resource.code == a), this.resourceData.find(resource => resource.code == b) )); + + deepfreeze(this.resourceData); + deepfreeze(this.resourceDataObj); + deepfreeze(this.resourceCodes); + deepfreeze(this.resourceNames); } /** * Returns the objects defined in the JSON files for all availbale resources, * ordered as defined in these files. */ Resources.prototype.GetResources = function() { return this.resourceData; }; /** * Returns the object defined in the JSON file for the given resource. */ Resources.prototype.GetResource = function(type) { return this.resourceDataObj[type]; }; /** * Returns an array containing all resource codes ordered as defined in the resource files. * For example ["food", "wood", "stone", "metal"]. */ Resources.prototype.GetCodes = function() { return this.resourceCodes; }; /** * Returns an object mapping resource codes to translatable resource names. Includes subtypes. * For example { "food": "Food", "fish": "Fish", "fruit": "Fruit", "metal": "Metal", ... } */ Resources.prototype.GetNames = function() { return this.resourceNames; }; Index: ps/trunk/binaries/data/mods/public/gui/common/functions_civinfo.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_civinfo.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_civinfo.js (revision 20100) @@ -1,34 +1,34 @@ /** * Loads history and some gameplay data for civs. * * @param selectableOnly {boolean} - Only load those which can be selected * in the gamesetup. Scenario maps might set non-selectable civs. * @param gaia {boolean} - Whether to include gaia as a mock civ. */ function loadCivData(selectableOnly, gaia) { let civData = {}; let civFiles = Engine.BuildDirEntList("simulation/data/civs/", "*.json", false); for (let filename of civFiles) { let data = Engine.ReadJSONFile(filename); if (!data) continue; translateObjectKeys(data, ["Name", "Description", "History", "Special"]); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; // Sanity check for (let prop of ["Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames","SelectableInGameSetup"]) if (data[prop] == undefined) error(filename + " doesn't contain " + prop); } if (gaia) civData.gaia = { "Code": "gaia", "Name": translate("Gaia") }; - return civData; + return deepfreeze(civData); } Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20100) @@ -1,423 +1,423 @@ /** * Highlights the victory condition in the game-description. */ var g_DescriptionHighlight = "orange"; /** * The rating assigned to lobby players who didn't complete a ranked 1v1 yet. */ var g_DefaultLobbyRating = 1200; /** * XEP-0172 doesn't restrict nicknames, but our lobby policy does. * So use this human readable delimiter to separate buddy names in the config file. */ var g_BuddyListDelimiter = ","; /** * Returns the nickname without the lobby rating. */ function splitRatingFromNick(playerName) { let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName); if (!result) return [playerName, ""]; return [result[1], +result[2]]; } /** * Array of playernames that the current user has marked as buddies. */ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); /** * Denotes which players are a lobby buddy of the current user. */ var g_BuddySymbol = '•'; /** * Returns map description and preview image or placeholder. */ function getMapDescriptionAndPreview(mapType, mapName) { let mapData; if (mapType == "random" && mapName == "random") mapData = { "settings": { "Description": translate("A randomly selected map.") } }; else if (mapType == "random" && Engine.FileExists(mapName + ".json")) mapData = Engine.ReadJSONFile(mapName + ".json"); else if (Engine.FileExists(mapName + ".xml")) mapData = Engine.LoadMapSettings(mapName + ".xml"); - return { + return deepfreeze({ "description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."), "preview": mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png" - }; + }); } /** * Sets the mappreview image correctly. * It needs to be cropped as the engine only allows loading square textures. * * @param {string} guiObject * @param {string} filename */ function setMapPreviewImage(guiObject, filename) { Engine.GetGUIObjectByName(guiObject).sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + "session/icons/mappreview/" + filename; } /** * Returns a formatted string describing the player assignments. * Needs g_CivData to translate! * * @param {object} playerDataArray - As known from gamesetup and simstate. * @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player. * @returns {string} */ function formatPlayerInfo(playerDataArray, playerStates) { let playerDescriptions = {}; let playerIdx = 0; for (let playerData of playerDataArray) { if (playerData == null || playerData.Civ && playerData.Civ == "gaia") continue; ++playerIdx; let teamIdx = playerData.Team; let isAI = playerData.AI && playerData.AI != ""; let playerState = playerStates && playerStates[playerIdx] || playerData.State; let isActive = !playerState || playerState == "active"; let playerDescription; if (isAI) { if (playerData.Civ) { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, %(state)s)"); } else { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s, %(state)s)"); } } else { if (playerData.Offline) { // Can only occur in the lobby for now, so no strings with civ needed if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)"); } else { if (playerData.Civ) if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)"); else if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(state)s)"); } } // Sort player descriptions by team if (!playerDescriptions[teamIdx]) playerDescriptions[teamIdx] = []; playerDescriptions[teamIdx].push(sprintf(playerDescription, { "playerName": '[color="' + (typeof getPlayerColor == 'function' ? (isAI ? "white" : getPlayerColor(playerData.Name)) : rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color)) + '"]' + (g_Buddies.indexOf(splitRatingFromNick(playerData.Name)[0]) != -1 ? g_BuddySymbol + " " : "") + escapeText(playerData.Name) + "[/color]", "civ": !playerData.Civ ? translate("Unknown Civilization") : g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ? translate(g_CivData[playerData.Civ].Name) : playerData.Civ, "state": playerState == "defeated" ? translateWithContext("playerstate", "defeated") : translateWithContext("playerstate", "won"), "AIname": isAI ? translateAIName(playerData.AI) : "", "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "" })); } let teams = Object.keys(playerDescriptions); if (teams.indexOf("observer") > -1) teams.splice(teams.indexOf("observer"), 1); let teamDescription = []; // If there are no teams, merge all playersDescriptions if (teams.length == 1) teamDescription.push(playerDescriptions[teams[0]].join("\n")); // If there are teams, merge "Team N:" + playerDescriptions else teamDescription = teams.map(team => { let teamCaption = team == -1 ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); // Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + teamCaption + "[/font]", "playerDescriptions": playerDescriptions[team].join("\n") }); }); if (playerDescriptions.observer) teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]", "playerDescriptions": playerDescriptions.observer.join("\n") })); return teamDescription.join("\n\n"); } /** * Sets an additional map label, map preview image and describes the chosen gamesettings more closely. * * Requires g_GameAttributes and g_VictoryConditions. */ function getGameDescription(extended = false) { let titles = []; let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default); if (victoryIdx != -1) { let title = g_VictoryConditions.Title[victoryIdx]; if (g_VictoryConditions.Name[victoryIdx] == "wonder") title = sprintf( translatePluralWithContext( "victory condition", "Wonder (%(min)s minute)", "Wonder (%(min)s minutes)", g_GameAttributes.settings.VictoryDuration ), { "min": g_GameAttributes.settings.VictoryDuration } ); let isCaptureTheRelic = g_VictoryConditions.Name[victoryIdx] == "capture_the_relic"; if (isCaptureTheRelic) title = sprintf( translatePluralWithContext( "victory condition", "Capture the Relic (%(min)s minute)", "Capture the Relic (%(min)s minutes)", g_GameAttributes.settings.VictoryDuration ), { "min": g_GameAttributes.settings.VictoryDuration } ); titles.push({ "label": title, "value": g_VictoryConditions.Description[victoryIdx] }); if (isCaptureTheRelic) titles.push({ "label": translate("Relic Count"), "value": g_GameAttributes.settings.RelicCount }); if (g_VictoryConditions.Name[victoryIdx] == "regicide") if (g_GameAttributes.settings.RegicideGarrison) titles.push({ "label": translate("Hero Garrison"), "value": translate("Heroes can be garrisoned.") }); else titles.push({ "label": translate("Exposed Heroes"), "value": translate("Heroes cannot be garrisoned, and they are vulnerable to raids.") }); } if (g_GameAttributes.settings.RatingEnabled && g_GameAttributes.settings.PlayerData.length == 2) titles.push({ "label": translate("Rated game"), "value": translate("When the winner of this match is determined, the lobby score will be adapted.") }); if (g_GameAttributes.settings.LockTeams) titles.push({ "label": translate("Locked Teams"), "value": translate("Players can't change the initial teams.") }); else titles.push({ "label": translate("Diplomacy"), "value": translate("Players can make alliances and declare war on allies.") }); if (g_GameAttributes.settings.LastManStanding) titles.push({ "label": translate("Last Man Standing"), "value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.") }); else titles.push({ "label": translate("Allied Victory"), "value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.") }); if (extended) { titles.push({ "label": translate("Ceasefire"), "value": g_GameAttributes.settings.Ceasefire == 0 ? translate("disabled") : sprintf(translatePlural( "For the first minute, other players will stay neutral.", "For the first %(min)s minutes, other players will stay neutral.", g_GameAttributes.settings.Ceasefire), { "min": g_GameAttributes.settings.Ceasefire }) }); titles.push({ "label": translate("Map Name"), "value": translate(g_GameAttributes.settings.Name) }); titles.push({ "label": translate("Map Type"), "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); if (g_GameAttributes.mapType == "random") { let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)]; if (mapSize) titles.push({ "label": translate("Map Size"), "value": mapSize }); } } titles.push({ "label": translate("Map Description"), "value": g_GameAttributes.map == "random" ? translate("Randomly selects a map from the list") : g_GameAttributes.settings.Description ? translate(g_GameAttributes.settings.Description) : translate("Sorry, no description available."), }); if (extended) { titles.push({ "label": translate("Starting Resources"), "value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { "startingResourcesTitle": g_StartingResources.Title[ g_StartingResources.Resources.indexOf( g_GameAttributes.settings.StartingResources)], "amount": g_GameAttributes.settings.StartingResources }) }); titles.push({ "label": translate("Population Limit"), "value": g_PopulationCapacities.Title[ g_PopulationCapacities.Population.indexOf( g_GameAttributes.settings.PopulationCap)] }); titles.push({ "label": translate("Treasures"), "value": g_GameAttributes.settings.DisableTreasures ? translateWithContext("treasures", "Disabled") : translateWithContext("treasures", "As defined by the map.") }); titles.push({ "label": translate("Revealed Map"), "value": g_GameAttributes.settings.RevealMap }); titles.push({ "label": translate("Explored Map"), "value": g_GameAttributes.settings.ExploreMap }); titles.push({ "label": translate("Cheats"), "value": g_GameAttributes.settings.CheatsEnabled }); } return titles.map(title => sprintf(translate("%(label)s %(details)s"), { "label": "[color=\"" + g_DescriptionHighlight + "\"]" + title.label + ":" + "[/color]", "details": title.value === true ? translateWithContext("gamesetup option", "enabled") : !title.value ? translateWithContext("gamesetup option", "disabled") : title.value })).join("\n"); } /** * Sets the win/defeat icon to indicate current player's state. * @param {string} state - The current in-game state of the player. * @param {string} imageID - The name of the XML image object to update. */ function setOutcomeIcon(state, imageID) { let image = Engine.GetGUIObjectByName(imageID); if (state == "won") { image.sprite = "stretched:session/icons/victory.png"; image.tooltip = translate("Victorious"); } else if (state == "defeated") { image.sprite = "stretched:session/icons/defeat.png"; image.tooltip = translate("Defeated"); } } Index: ps/trunk/binaries/data/mods/public/gui/common/settings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 20100) @@ -1,381 +1,381 @@ /** * The maximum number of players that the engine supports. * TODO: Maybe we can support more than 8 players sometime. */ const g_MaxPlayers = 8; /** * The maximum number of teams allowed. */ const g_MaxTeams = 4; /** * Directory containing all editable settings. */ const g_SettingsDirectory = "simulation/data/settings/"; /** * An object containing all values given by setting name. * Used by lobby, gamesetup, session, summary screen and replay menu. */ const g_Settings = loadSettingsValues(); /** * Loads and translates all values of all settings which * can be configured by dropdowns in the gamesetup. * * @returns {Object|undefined} */ function loadSettingsValues() { var settings = { "AIDescriptions": loadAIDescriptions(), "AIDifficulties": loadAIDifficulties(), "Ceasefire": loadCeasefire(), "VictoryDurations": loadVictoryDuration(), "GameSpeeds": loadSettingValuesFile("game_speeds.json"), "MapTypes": loadMapTypes(), "MapSizes": loadSettingValuesFile("map_sizes.json"), "PlayerDefaults": loadPlayerDefaults(), "PopulationCapacities": loadPopulationCapacities(), "StartingResources": loadSettingValuesFile("starting_resources.json"), "VictoryConditions": loadVictoryConditions() }; if (Object.keys(settings).some(key => settings[key] === undefined)) return undefined; - return settings; + return deepfreeze(settings); } /** * Returns an array of objects reflecting all possible values for a given setting. * * @param {string} filename * @see simulation/data/settings/ * @returns {Array|undefined} */ function loadSettingValuesFile(filename) { var json = Engine.ReadJSONFile(g_SettingsDirectory + filename); if (!json || !json.Data) { error("Could not load " + filename + "!"); return undefined; } if (json.TranslatedKeys) { let keyContext = json.TranslatedKeys; if (json.TranslationContext) { keyContext = {}; for (let key of json.TranslatedKeys) keyContext[key] = json.TranslationContext; } translateObjectKeys(json.Data, keyContext); } return json.Data; } /** * Loads the descriptions as defined in simulation/ai/.../data.json and loaded by ICmpAIManager.cpp. * * @returns {Array} */ function loadAIDescriptions() { var ais = Engine.GetAIs(); translateObjectKeys(ais, ["name", "description"]); return ais.sort((a, b) => a.data.name.localeCompare(b.data.name)); } /** * Hardcoded, as modding is not supported without major changes. * Notice the AI code parses the difficulty level by the index, not by name. * * @returns {Array} */ function loadAIDifficulties() { return [ { "Name": "sandbox", "Title": translateWithContext("aiDiff", "Sandbox") }, { "Name": "very easy", "Title": translateWithContext("aiDiff", "Very Easy") }, { "Name": "easy", "Title": translateWithContext("aiDiff", "Easy") }, { "Name": "medium", "Title": translateWithContext("aiDiff", "Medium"), "Default": true }, { "Name": "hard", "Title": translateWithContext("aiDiff", "Hard") }, { "Name": "very hard", "Title": translateWithContext("aiDiff", "Very Hard") } ]; } /** * Loads available victory times for victory conditions like Wonder and Capture the Relic. */ function loadVictoryDuration() { var jsonFile = "victory_times.json"; var json = Engine.ReadJSONFile(g_SettingsDirectory + jsonFile); if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times)) { error("Could not load " + jsonFile); return undefined; } return json.Times.map(duration => ({ "Duration": duration, "Default": duration == json.Default, "Title": sprintf(translatePluralWithContext("victory duration", "%(min)s minute", "%(min)s minutes", duration), { "min": duration }) })); } /** * Loads available ceasefire settings. * * @returns {Array|undefined} */ function loadCeasefire() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "ceasefire.json"); if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times)) { error("Could not load ceasefire.json"); return undefined; } return json.Times.map(timeout => ({ "Duration": timeout, "Default": timeout == json.Default, "Title": timeout == 0 ? translateWithContext("ceasefire", "No ceasefire") : sprintf(translatePluralWithContext("ceasefire", "%(minutes)s minute", "%(minutes)s minutes", timeout), { "minutes": timeout }) })); } /** * Hardcoded, as modding is not supported without major changes. * * @returns {Array} */ function loadMapTypes() { return [ { "Name": "skirmish", "Title": translateWithContext("map", "Skirmish"), "Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."), "Default": true }, { "Name": "random", "Title": translateWithContext("map", "Random"), "Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.") }, { "Name": "scenario", "Title": translateWithContext("map", "Scenario"), "Description": translate("A map with a predefined landscape and matchsettings.") } ]; } /** * Loads available gametypes. * * @returns {Array|undefined} */ function loadVictoryConditions() { let subdir = "victory_conditions/"; let files = Engine.BuildDirEntList(g_SettingsDirectory + subdir, "*.json", false).map( file => file.substr(g_SettingsDirectory.length)); let victoryConditions = files.map(file => { let vc = loadSettingValuesFile(file); if (vc) vc.Name = file.substr(subdir.length, file.length - (subdir + ".json").length); return vc; }); if (victoryConditions.some(vc => vc == undefined)) return undefined; // TODO: We might support enabling victory conditions separately sometime. // Until then, we supplement the endless gametype here. victoryConditions.push({ "Name": "endless", "Title": translateWithContext("victory condition", "None"), "Description": translate("Endless game."), "Scripts": [] }); return victoryConditions; } /** * Loads the default player settings (like civs and colors). * * @returns {Array|undefined} */ function loadPlayerDefaults() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "player_defaults.json"); if (!json || !json.PlayerData) { error("Could not load player_defaults.json"); return undefined; } return json.PlayerData; } /** * Loads available population capacities. * * @returns {Array|undefined} */ function loadPopulationCapacities() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json"); if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities)) { error("Could not load population_capacities.json"); return undefined; } return json.PopulationCapacities.map(population => ({ "Population": population, "Default": population == json.Default, "Title": population < 10000 ? population : translate("Unlimited") })); } /** * Creates an object with all values of that property of the given setting and * finds the index of the default value. * * This allows easy copying of setting values to dropdown lists. * * @param {Array} settingValues * @returns {Object|undefined} */ function prepareForDropdown(settingValues) { if (!settingValues) return undefined; let settings = { "Default": 0 }; for (let index in settingValues) { for (let property in settingValues[index]) { if (property == "Default") continue; if (!settings[property]) settings[property] = []; // Switch property and index settings[property][index] = settingValues[index][property]; } // Copy default value if (settingValues[index].Default) settings.Default = +index; } - return settings; + return deepfreeze(settings); } /** * Returns title or placeholder. * * @param {string} aiName - for example "petra" */ function translateAIName(aiName) { let description = g_Settings.AIDescriptions.find(ai => ai.id == aiName); return description ? translate(description.data.name) : translateWithContext("AI name", "Unknown"); } /** * Returns title or placeholder. * * @param {Number} index - index of AIDifficulties */ function translateAIDifficulty(index) { let difficulty = g_Settings.AIDifficulties[index]; return difficulty ? difficulty.Title : translateWithContext("AI difficulty", "Unknown"); } /** * Returns title or placeholder. * * @param {string} mapType - for example "skirmish" * @returns {string} */ function translateMapType(mapType) { let type = g_Settings.MapTypes.find(t => t.Name == mapType); return type ? type.Title : translateWithContext("map type", "Unknown"); } /** * Returns title or placeholder "Default". * * @param {Number} mapSize - tilecount * @returns {string} */ function translateMapSize(tiles) { let mapSize = g_Settings.MapSizes.find(mapSize => mapSize.Tiles == +tiles); return mapSize ? mapSize.Name : translateWithContext("map size", "Default"); } /** * Returns title or placeholder. * * @param {Number} population - for example 300 * @returns {string} */ function translatePopulationCapacity(population) { let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population); return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown"); } /** * Returns title or placeholder. * * @param {string} gameType - for example "conquest" * @returns {string} */ function translateVictoryCondition(gameType) { let vc = g_Settings.VictoryConditions.find(vc => vc.Name == gameType); return vc ? vc.Title : translateWithContext("victory condition", "Unknown"); } Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20100) @@ -1,2342 +1,2343 @@ const g_MatchSettings_SP = "config/matchsettings.json"; const g_MatchSettings_MP = "config/matchsettings.mp.json"; const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly)); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); /** * Offer users to select playable civs only. * Load unselectable civs as they could appear in scenario maps. */ const g_CivData = loadCivData(false, false); /** * Highlight the "random" dropdownlist item. */ var g_ColorRandom = "orange"; /** * Color for regular dropdownlist items. */ var g_ColorRegular = "white"; /** * Color for "Unassigned"-placeholder item in the dropdownlist. */ var g_PlayerAssignmentColors = { "player": g_ColorRegular, "observer": "170 170 250", "unassigned": "140 140 140", "AI": "70 150 70" }; /** * Used for highlighting the sender of chat messages. */ var g_SenderFont = "sans-bold-13"; /** * This yields [1, 2, ..., MaxPlayers]. */ var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); /** * Used for generating the botnames. */ var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"]; var g_PlayerTeamList = prepareForDropdown([{ "label": translateWithContext("team", "None"), "id": -1 }].concat( Array(g_MaxTeams).fill(0).map((v, i) => ({ "label": i + 1, "id": i })) ) ); /** * Number of relics: [1, ..., NumCivs] */ var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1); var g_PlayerCivList = g_CivData && prepareForDropdown([{ "name": translateWithContext("civilization", "Random"), "tooltip": translate("Picks one civilization at random when the game starts."), "color": g_ColorRandom, "code": "random" }].concat( Object.keys(g_CivData).filter( civ => g_CivData[civ].SelectableInGameSetup ).map(civ => ({ "name": g_CivData[civ].Name, "tooltip": g_CivData[civ].History, "color": g_ColorRegular, "code": civ })).sort(sortNameIgnoreCase) ) ); /** * All selectable playercolors except gaia. */ var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color); /** * Directory containing all maps of the given type. */ var g_MapPath = { "random": "maps/random/", "scenario": "maps/scenarios/", "skirmish": "maps/skirmishes/" }; /** * Containing the colors to highlight the ready status of players, * the chat ready messages and * the tooltips and captions for the ready button */ var g_ReadyData = [ { "color": g_ColorRegular, "chat": translate("* %(username)s is not ready."), "caption": translate("I'm ready"), "tooltip": translate("State that you are ready to play.") }, { "color": "green", "chat": translate("* %(username)s is ready!"), "caption": translate("Stay ready"), "tooltip": translate("Stay ready even when the game settings change.") }, { "color": "150 150 250", "chat": "", "caption": translate("I'm not ready!"), "tooltip": translate("State that you are not ready to play.") } ]; /** * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. */ var g_NetMessageTypes = { "netstatus": msg => handleNetStatusMessage(msg), "netwarn": msg => addNetworkWarning(msg), "gamesetup": msg => handleGamesetupMessage(msg), "players": msg => handlePlayerAssignmentMessage(msg), "ready": msg => handleReadyMessage(msg), "start": msg => handleGamestartMessage(msg), "kicked": msg => addChatMessage({ "type": msg.banned ? "banned" : "kicked", "username": msg.username }), "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }), }; var g_FormatChatMessage = { "system": (msg, user) => systemMessage(msg.text), "settings": (msg, user) => systemMessage(translate('Game settings have been changed')), "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })), "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })), "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })), "banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })), "chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), { "username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })), "message": escapeText(msg.text || "") }), "ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }), "clientlist": (msg, user) => getUsernameList(), }; var g_MapFilters = [ { "id": "default", "name": translateWithContext("map filter", "Default"), "tooltip": translateWithContext("map filter", "All maps except naval and demo maps."), "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1), "Default": true }, { "id": "naval", "name": translate("Naval Maps"), "tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."), "filter": mapKeywords => mapKeywords.indexOf("naval") != -1 }, { "id": "demo", "name": translate("Demo Maps"), "tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."), "filter": mapKeywords => mapKeywords.indexOf("demo") != -1 }, { "id": "new", "name": translate("New Maps"), "tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."), "filter": mapKeywords => mapKeywords.indexOf("new") != -1 }, { "id": "trigger", "name": translate("Trigger Maps"), "tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."), "filter": mapKeywords => mapKeywords.indexOf("trigger") != -1 }, { "id": "all", "name": translate("All Maps"), "tooltip": translateWithContext("map filter", "Every map of the chosen maptype."), "filter": mapKeywords => true }, ]; /** * This contains only filters that have at least one map matching them. */ var g_MapFilterList; /** * Whether this is a single- or multiplayer match. */ var g_IsNetworked; /** * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame). */ var g_IsController; /** * Whether this is a tutorial. */ var g_IsTutorial; /** * To report the game to the lobby bot. */ var g_ServerName; var g_ServerPort; /** * IP address and port of the STUN endpoint. */ var g_StunEndpoint; /** * Current username. Cannot contain whitespace. */ var g_Username = Engine.LobbyGetNick(); /** * States whether the GUI is currently updated in response to network messages instead of user input * and therefore shouldn't send further messages to the network. */ var g_IsInGuiUpdate = false; /** * Whether the current player is ready to start the game. * 0 - not ready * 1 - ready * 2 - stay ready */ var g_IsReady = 0; /** * Ignore duplicate ready commands on init. */ var g_ReadyInit = true; /** * If noone has changed the ready status, we have no need to spam the settings changed message. * * <=0 - Suppressed settings message * 1 - Will show settings message * 2 - Host's initial ready, suppressed settings message */ var g_ReadyChanged = 2; /** * Used to prevent calling resetReadyData when starting a game. */ var g_GameStarted = false; /** * Selectable options (player, AI, unassigned) in the player assignment dropdowns and * their colorized, textual representation. */ var g_PlayerAssignmentList = {}; /** * Remembers which clients are assigned to which player slots and whether they are ready. * The keys are guids or "local" in Singleplayer. */ var g_PlayerAssignments = {}; var g_DefaultPlayerData = []; var g_GameAttributes = { "settings": {} }; /** * List of translated words that can be used to autocomplete titles of settings * and their values (for example playernames). */ var g_Autocomplete = []; /** * Array of strings formatted as displayed, including playername. */ var g_ChatMessages = []; /** * Filename and translated title of all maps, given the currently selected * maptype and filter. Sorted by title, shown in the dropdown. */ var g_MapSelectionList = []; /** * Cache containing the mapsettings. Just-in-time loading. */ var g_MapData = {}; /** * Wait one tick before initializing the GUI objects and * don't process netmessages prior to that. */ var g_LoadingState = 0; /** * Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds. */ var g_GameStanzaTimeout = 2; /** * Index of the GUI timer. */ var g_GameStanzaTimer; /** * Only send a lobby update if something actually changed. */ var g_LastGameStanza; /** * Remembers if the current player viewed the AI settings of some playerslot. */ var g_LastViewedAIPlayer = -1; /** * Total number of units that the engine can run with smoothly. * It means a 4v4 with 150 population can still run nicely, but more than that might "lag". */ var g_PopulationCapacityRecommendation = 1200; /** * Order in which the GUI elements will be shown. * All valid options are required to appear here. * The ones under "map" are shown in the map selection panel, * the others in the "more options" dialog. */ var g_OptionOrderGUI = { "map": { "Dropdown": [ "mapType", "mapFilter", "mapSelection", "numPlayers", "mapSize", ], "Checkbox": [ ], }, "more": { "Dropdown": [ "gameSpeed", "victoryCondition", "relicCount", "victoryDuration", "populationCap", "startingResources", "ceasefire", ], "Checkbox": [ "regicideGarrison", "exploreMap", "revealMap", "disableTreasures", "disableSpies", "lockTeams", "lastManStanding", "enableCheats", "enableRating", ] } }; /** * These options must be initialized first, in the given order. */ var g_OptionOrderInit = { "dropdowns": [ "mapType", "mapFilter", "mapSelection" ], "checkboxes": [ ] }; /** * Contains the logic of all multiple-choice gamesettings. * * Logic * ids - Array of identifier strings that indicate the selected value. * default - Returns the index of the default value (not the value itself). * defined - Whether a value for the setting is actually specified. * get - The identifier of the currently selected value. * select - Saves and processes the value of the selected index of the ids array. * * GUI * title - The caption shown in the label. * tooltip - A description shown when hovering the dropdown or a specific item. * labels - Array of translated strings selectable for this dropdown. * colors - Optional array of colors to tint the according dropdown items with. * hidden - If hidden, both the label and dropdown won't be visible. (default: false) * enabled - Only the label will be shown if the setting is disabled. (default: true) * autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined) * If not undefined, must be a number that denotes the priority (higher numbers come first). * If undefined, still autocompletes the translated title of the setting. */ var g_Dropdowns = { "mapType": { "title": () => translate("Map Type"), "tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."), "labels": () => g_MapTypes.Title, "ids": () => g_MapTypes.Name, "default": () => g_MapTypes.Default, "defined": () => g_GameAttributes.mapType !== undefined, "get": () => g_GameAttributes.mapType, "select": (idx) => { g_MapData = {}; g_GameAttributes.mapType = g_MapTypes.Name[idx]; g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType]; delete g_GameAttributes.map; if (g_GameAttributes.mapType != "scenario") g_GameAttributes.settings = { "PlayerData": g_DefaultPlayerData.slice(0, 4) }; reloadMapFilterList(); }, "autocomplete": 0, }, "mapFilter": { "title": () => translate("Map Filter"), "tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."), "labels": () => g_MapFilterList.name, "ids": () => g_MapFilterList.id, "default": () => g_MapFilterList.Default, "defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1, "get": () => g_GameAttributes.mapFilter, "select": (idx) => { g_GameAttributes.mapFilter = g_MapFilterList.id[idx]; delete g_GameAttributes.map; reloadMapList(); }, "autocomplete": 0, }, "mapSelection": { "title": () => translate("Select Map"), "tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."), "labels": () => g_MapSelectionList.name, "colors": () => g_MapSelectionList.color, "ids": () => g_MapSelectionList.file, "default": () => 0, "defined": () => g_GameAttributes.map !== undefined, "get": () => g_GameAttributes.map, "select": (idx) => { selectMap(g_MapSelectionList.file[idx]); }, "autocomplete": 0, }, "mapSize": { "title": () => translate("Map Size"), "tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"), "labels": () => g_MapSizes.Name, "ids": () => g_MapSizes.Tiles, "default": () => g_MapSizes.Default, "defined": () => g_GameAttributes.settings.Size !== undefined, "get": () => g_GameAttributes.settings.Size, "select": (idx) => { g_GameAttributes.settings.Size = g_MapSizes.Tiles[idx]; }, "hidden": () => g_GameAttributes.mapType != "random", "autocomplete": 0, }, "numPlayers": { "title": () => translate("Number of Players"), "tooltip": (hoverIdx) => translate("Select number of players."), "labels": () => g_NumPlayersList, "ids": () => g_NumPlayersList, "default": () => g_MaxPlayers - 1, "defined": () => g_GameAttributes.settings.PlayerData !== undefined, "get": () => g_GameAttributes.settings.PlayerData.length, "enabled": () => g_GameAttributes.mapType == "random", "select": (idx) => { let num = idx + 1; let pData = g_GameAttributes.settings.PlayerData; g_GameAttributes.settings.PlayerData = num > pData.length ? pData.concat(g_DefaultPlayerData.slice(pData.length, num)) : pData.slice(0, num); unassignInvalidPlayers(num); sanitizePlayerData(g_GameAttributes.settings.PlayerData); }, }, "populationCap": { "title": () => translate("Population Cap"), "tooltip": (hoverIdx) => { let popCap = g_PopulationCapacities.Population[hoverIdx]; let players = g_GameAttributes.settings.PlayerData.length; if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation) return translate("Select population limit."); return '[color="orange"]' + sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), { "players": players, "popCap": popCap }) + '[/color]'; }, "labels": () => g_PopulationCapacities.Title, "ids": () => g_PopulationCapacities.Population, "default": () => g_PopulationCapacities.Default, "defined": () => g_GameAttributes.settings.PopulationCap !== undefined, "get": () => g_GameAttributes.settings.PopulationCap, "select": (idx) => { g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[idx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "startingResources": { "title": () => translate("Starting Resources"), "tooltip": (hoverIdx) => { return hoverIdx >= 0 ? sprintf(translate("Initial amount of each resource: %(resources)s."), { "resources": g_StartingResources.Resources[hoverIdx] }) : translate("Select the game's starting resources."); }, "labels": () => g_StartingResources.Title, "ids": () => g_StartingResources.Resources, "default": () => g_StartingResources.Default, "defined": () => g_GameAttributes.settings.StartingResources !== undefined, "get": () => g_GameAttributes.settings.StartingResources, "select": (idx) => { g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[idx]; }, "hidden": () => g_GameAttributes.mapType == "scenario", "autocomplete": 0, }, "ceasefire": { "title": () => translate("Ceasefire"), "tooltip": (hoverIdx) => translate("Set time where no attacks are possible."), "labels": () => g_Ceasefire.Title, "ids": () => g_Ceasefire.Duration, "default": () => g_Ceasefire.Default, "defined": () => g_GameAttributes.settings.Ceasefire !== undefined, "get": () => g_GameAttributes.settings.Ceasefire, "select": (idx) => { g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[idx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "victoryCondition": { "title": () => translate("Victory Condition"), "tooltip": (hoverIdx) => g_VictoryConditions.Description[hoverIdx] || translate("Select victory condition."), "labels": () => g_VictoryConditions.Title, "ids": () => g_VictoryConditions.Name, "default": () => g_VictoryConditions.Default, "defined": () => g_GameAttributes.settings.GameType !== undefined, "get": () => g_GameAttributes.settings.GameType, "select": (idx) => { g_GameAttributes.settings.GameType = g_VictoryConditions.Name[idx]; g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[idx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "autocomplete": 0, }, "relicCount": { "title": () => translate("Relic Count"), "tooltip": (hoverIdx) => translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses."), "labels": () => g_RelicCountList, "ids": () => g_RelicCountList, "default": () => g_RelicCountList.indexOf(2), "defined": () => g_GameAttributes.settings.RelicCount !== undefined, "get": () => g_GameAttributes.settings.RelicCount, "select": (idx) => { g_GameAttributes.settings.RelicCount = g_RelicCountList[idx]; }, "hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic", "enabled": () => g_GameAttributes.mapType != "scenario", }, "victoryDuration": { "title": () => translate("Victory Duration"), "tooltip": (hoverIdx) => translate("Number of minutes until the player has won."), "labels": () => g_VictoryDurations.Title, "ids": () => g_VictoryDurations.Duration, "default": () => g_VictoryDurations.Default, "defined": () => g_GameAttributes.settings.VictoryDuration !== undefined, "get": () => g_GameAttributes.settings.VictoryDuration, "select": (idx) => { g_GameAttributes.settings.VictoryDuration = g_VictoryDurations.Duration[idx]; }, "hidden": () => g_GameAttributes.settings.GameType != "wonder" && g_GameAttributes.settings.GameType != "capture_the_relic", "enabled": () => g_GameAttributes.mapType != "scenario", }, "gameSpeed": { "title": () => translate("Game Speed"), "tooltip": (hoverIdx) => translate("Select game speed."), "labels": () => g_GameSpeeds.Title, "ids": () => g_GameSpeeds.Speed, "default": () => g_GameSpeeds.Default, "defined": () => g_GameAttributes.gameSpeed !== undefined, "get": () => g_GameAttributes.gameSpeed, "select": (idx) => { g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[idx]; }, }, }; /** * These dropdowns provide a setting that is repeated once for each player * (where idx is the playerID starting from 0 for player 1). */ var g_PlayerDropdowns = { "playerAssignment": { "labels": (idx) => g_PlayerAssignmentList.Name || [], "colors": (idx) => g_PlayerAssignmentList.Color || [], "ids": (idx) => g_PlayerAssignmentList.Choice || [], "default": (idx) => "ai:petra", "defined": (idx) => idx < g_GameAttributes.settings.PlayerData.length, "get": (idx) => { for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player == idx + 1) return "guid:" + guid; for (let ai of g_Settings.AIDescriptions) if (g_GameAttributes.settings.PlayerData[idx].AI == ai.id) return "ai:" + ai.id; return "unassigned"; }, "select": (selectedIdx, idx) => { let choice = g_PlayerAssignmentList.Choice[selectedIdx]; if (choice == "unassigned" || choice.startsWith("ai:")) { if (g_IsNetworked) Engine.AssignNetworkPlayer(idx+1, ""); else if (g_PlayerAssignments.local.player == idx+1) g_PlayerAssignments.local.player = -1; g_GameAttributes.settings.PlayerData[idx].AI = choice.startsWith("ai:") ? choice.substr(3) : ""; } else swapPlayers(choice.substr("guid:".length), idx); }, "autocomplete": 100, }, "playerTeam": { "labels": (idx) => g_PlayerTeamList.label, "ids": (idx) => g_PlayerTeamList.id, "default": (idx) => 0, "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Team !== undefined, "get": (idx) => g_GameAttributes.settings.PlayerData[idx].Team, "select": (selectedIdx, idx) => { g_GameAttributes.settings.PlayerData[idx].Team = selectedIdx - 1; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "playerCiv": { "tooltip": (hoverIdx, idx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Chose the civilization for this player"), "labels": (idx) => g_PlayerCivList.name, "colors": (idx) => g_PlayerCivList.color, "ids": (idx) => g_PlayerCivList.code, "default": (idx) => 0, "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Civ !== undefined, "get": (idx) => g_GameAttributes.settings.PlayerData[idx].Civ, "select": (selectedIdx, idx) => { g_GameAttributes.settings.PlayerData[idx].Civ = g_PlayerCivList.code[selectedIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "autocomplete": 0, }, "playerColorPicker": { "labels": (idx) => g_PlayerColorPickerList.map(color => "■"), "colors": (idx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)), "ids": (idx) => g_PlayerColorPickerList.map((color, index) => index), "default": (idx) => idx, "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Color !== undefined, "get": (idx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[idx].Color), "select": (selectedIdx, idx) => { let playerData = g_GameAttributes.settings.PlayerData; // If someone else has that color, give that player the old color let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color)); if (sameColorPData) sameColorPData.Color = playerData[idx].Color; playerData[idx].Color = g_PlayerColorPickerList[selectedIdx]; ensureUniquePlayerColors(playerData); }, "enabled": () => g_GameAttributes.mapType != "scenario", }, }; /** * Contains the logic of all boolean gamesettings. */ var g_Checkboxes = { "regicideGarrison": { "title": () => translate("Hero Garrison"), "tooltip": () => translate("Toggle whether heroes can be garrisoned."), "default": () => false, "defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined, "get": () => g_GameAttributes.settings.RegicideGarrison, "set": checked => { g_GameAttributes.settings.RegicideGarrison = checked; }, "hidden": () => g_GameAttributes.settings.GameType != "regicide", "enabled": () => g_GameAttributes.mapType != "scenario", }, "revealMap": { "title": () => // Translation: Make sure to differentiate between the revealed map and explored map options! translate("Revealed Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Toggle revealed map (see everything)."), "default": () => false, "defined": () => g_GameAttributes.settings.RevealMap !== undefined, "get": () => g_GameAttributes.settings.RevealMap, "set": checked => { g_GameAttributes.settings.RevealMap = checked; if (checked) g_Checkboxes.exploreMap.set(true); }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "exploreMap": { "title": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Explored Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Toggle explored map (see initial map)."), "default": () => false, "defined": () => g_GameAttributes.settings.ExploreMap !== undefined, "get": () => g_GameAttributes.settings.ExploreMap, "set": checked => { g_GameAttributes.settings.ExploreMap = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap, }, "disableTreasures": { "title": () => translate("Disable Treasures"), "tooltip": () => translate("Disable all treasures on the map."), "default": () => false, "defined": () => g_GameAttributes.settings.DisableTreasures !== undefined, "get": () => g_GameAttributes.settings.DisableTreasures, "set": checked => { g_GameAttributes.settings.DisableTreasures = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "disableSpies": { "title": () => translate("Disable Spies"), "tooltip": () => translate("Disable spies during the game."), "default": () => false, "defined": () => g_GameAttributes.settings.DisableSpies !== undefined, "get": () => g_GameAttributes.settings.DisableSpies, "set": checked => { g_GameAttributes.settings.DisableSpies = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "lockTeams": { "title": () => translate("Teams Locked"), "tooltip": () => translate("Toggle locked teams."), "default": () => Engine.HasXmppClient(), "defined": () => g_GameAttributes.settings.LockTeams !== undefined, "get": () => g_GameAttributes.settings.LockTeams, "set": checked => { g_GameAttributes.settings.LockTeams = checked; g_GameAttributes.settings.LastManStanding = false; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RatingEnabled, }, "lastManStanding": { "title": () => translate("Last Man Standing"), "tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."), "default": () => false, "defined": () => g_GameAttributes.settings.LastManStanding !== undefined, "get": () => g_GameAttributes.settings.LastManStanding, "set": checked => { g_GameAttributes.settings.LastManStanding = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.LockTeams, }, "enableCheats": { "title": () => translate("Cheats"), "tooltip": () => translate("Toggle the usability of cheats."), "default": () => !g_IsNetworked, "hidden": () => !g_IsNetworked, "defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined, "get": () => g_GameAttributes.settings.CheatsEnabled, "set": checked => { g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked || checked && !g_GameAttributes.settings.RatingEnabled; }, "enabled": () => !g_GameAttributes.settings.RatingEnabled, }, "enableRating": { "title": () => translate("Rated Game"), "tooltip": () => translate("Toggle if this game will be rated for the leaderboard."), "default": () => Engine.HasXmppClient(), "hidden": () => !Engine.HasXmppClient(), "defined": () => g_GameAttributes.settings.RatingEnabled !== undefined, "get": () => !!g_GameAttributes.settings.RatingEnabled, "set": checked => { g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined; Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled); if (checked) { g_Checkboxes.lockTeams.set(true); g_Checkboxes.enableCheats.set(false); } }, }, }; /** * For setting up arbitrary GUI objects. */ var g_MiscControls = { "chatPanel": { "hidden": () => !g_IsNetworked, }, "chatInput": { "tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")), }, "cheatWarningText": { "hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled, }, "cancelGame": { "tooltip": () => Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu."), }, "startGame": { "caption": () => g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption, "tooltip": (hoverIdx) => !g_IsController ? g_ReadyData[g_IsReady].tooltip : !g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ? translate("Start a new game with the current settings.") : translate("Start a new game with the current settings (disabled until all players are ready)"), "enabled": () => !g_IsController || Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1 || guid == Engine.GetPlayerGUID() && g_IsController), "hidden": () => !g_PlayerAssignments[Engine.GetPlayerGUID()] || g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController, }, "civResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, "teamResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, // Display these after having hidden every GUI object in the "More Options" dialog "moreOptionsLabel": { "hidden": () => false, }, "hideMoreOptions": { "hidden": () => false, }, }; /** * Contains options that are repeated for every player. */ var g_PlayerMiscControls = { "playerBox": { "size": (idx) => ["0", 32 * idx, "100%", 32 * (idx + 1)].join(" "), }, "playerName": { "caption": (idx) => { let pData = g_GameAttributes.settings.PlayerData[idx]; let assignedGUID = Object.keys(g_PlayerAssignments).find( guid => g_PlayerAssignments[guid].player == idx + 1); let name = translate(pData.Name || g_DefaultPlayerData[idx].Name); if (g_IsNetworked) name = '[color="' + g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color + '"]' + name + '[/color]'; return name; }, }, "playerColor": { "sprite": (idx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[idx].Color) + " 100", }, "playerConfig": { "hidden": (idx) => !g_GameAttributes.settings.PlayerData[idx].AI, "onPress": (idx) => function() { openAIConfig(idx); }, }, }; /** * Initializes some globals without touching the GUI. * * @param {Object} attribs - context data sent by the lobby / mainmenu */ function init(attribs) { if (!g_Settings) { cancelSetup(); return; } if (["offline", "server", "client"].indexOf(attribs.type) == -1) { error("Unexpected 'type' in gamesetup init: " + attribs.type); cancelSetup(); return; } g_IsNetworked = attribs.type != "offline"; g_IsController = attribs.type != "client"; g_IsTutorial = !!attribs.tutorial; g_ServerName = attribs.serverName; g_ServerPort = attribs.serverPort; g_StunEndpoint = attribs.stunEndpoint; if (!g_IsNetworked) g_PlayerAssignments = { "local": { "name": singleplayerName(), "player": 1 } }; // Replace empty playername when entering a singleplayermatch for the first time if (!g_IsNetworked) { Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName()); Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg"); } initDefaults(); supplementDefaults(); setTimeout(displayGamestateNotifications, 1000); } function initDefaults() { // Remove gaia from both arrays - g_DefaultPlayerData = g_Settings.PlayerDefaults; - g_DefaultPlayerData.shift(); + g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1)); // Don't change the underlying defaults file, as Atlas uses that file too for (let i in g_DefaultPlayerData) { g_DefaultPlayerData[i].Civ = "random"; g_DefaultPlayerData[i].Team = -1; } + + g_DefaultPlayerData = deepfreeze(g_DefaultPlayerData); } /** * Sets default values for all g_GameAttribute settings which don't have a value set. */ function supplementDefaults() { for (let dropdown in g_Dropdowns) if (!g_Dropdowns[dropdown].defined()) g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default()); for (let checkbox in g_Checkboxes) if (!g_Checkboxes[checkbox].defined()) g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default()); for (let dropdown in g_PlayerDropdowns) for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i) if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i)) g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i); } /** * Called after the first tick. */ function initGUIObjects() { for (let dropdown of g_OptionOrderInit.dropdowns) initDropdown(dropdown); for (let dropdown in g_Dropdowns) if (g_OptionOrderInit.dropdowns.indexOf(dropdown) == -1) initDropdown(dropdown); for (let checkbox of g_OptionOrderInit.checkboxes) initCheckbox(checkbox); for (let checkbox in g_Checkboxes) if (g_OptionOrderInit.checkboxes.indexOf(checkbox) == -1) initCheckbox(checkbox); for (let dropdown in g_PlayerDropdowns) initPlayerDropdowns(dropdown); resizeMoreOptionsWindow(); initSPTips(); loadPersistMatchSettings(); updateGameAttributes(); sendRegisterGameStanzaImmediate(); if (g_IsTutorial) { launchTutorial(); return; } // Don't lift the curtain until the controls are updated the first time if (!g_IsNetworked) hideLoadingWindow(); } function hideLoadingWindow() { let loadingWindow = Engine.GetGUIObjectByName("loadingWindow"); if (loadingWindow.hidden) return; loadingWindow.hidden = true; Engine.GetGUIObjectByName("setupWindow").hidden = false; Engine.GetGUIObjectByName("chatInput").focus(); } /** * The main options (like map selection) and player arrays have specific names. * Options in the "More Options" dialog use a generic name. */ function getGUIObjectNameFromSetting(name) { for (let panel in g_OptionOrderGUI) for (let type in g_OptionOrderGUI[panel]) { let idx = g_OptionOrderGUI[panel][type].indexOf(name); if (idx != -1) return [panel + "Option" + type, "[" + idx + "]"]; } // Assume there is a GUI object with exactly that setting name return [name, ""]; } function initDropdown(name, idx) { let [guiName, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = idx === undefined ? "": "[" + idx + "]"; let data = (idx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let dropdown = Engine.GetGUIObjectByName(guiName + guiIdx + idxName); dropdown.list = data.labels(idx).map((label, id) => data.colors && data.colors(idx) ? '[color="' + data.colors(idx)[id] + '"]' + label + "[/color]" : label); dropdown.list_data = data.ids(idx); dropdown.onSelectionChange = function() { if (!g_IsController || g_IsInGuiUpdate || !this.list_data[this.selected] || data.hidden && data.hidden(idx) || data.enabled && !data.enabled(idx)) return; data.select(this.selected, idx); supplementDefaults(); updateGameAttributes(); }; if (data.tooltip) dropdown.onHoverChange = function() { this.tooltip = data.tooltip(this.hovered, idx); }; } function initPlayerDropdowns(name) { for (let i = 0; i < g_MaxPlayers; ++i) initDropdown(name, i); } function initCheckbox(name) { let [guiName, guiIdx] = getGUIObjectNameFromSetting(name); Engine.GetGUIObjectByName(guiName + guiIdx).onPress = function() { let obj = g_Checkboxes[name]; if (!g_IsController || g_IsInGuiUpdate || obj.enabled && !obj.enabled() || obj.hidden && obj.hidden()) return; obj.set(this.checked); supplementDefaults(); updateGameAttributes(); }; } function initSPTips() { if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true") return; Engine.GetGUIObjectByName("spTips").hidden = false; Engine.GetGUIObjectByName("displaySPTips").checked = true; Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt")); } function saveSPTipsSetting() { let enabled = String(Engine.GetGUIObjectByName("displaySPTips").checked); Engine.ConfigDB_CreateValue("user", "gui.gamesetup.enabletips", enabled); Engine.ConfigDB_WriteValueToFile("user", "gui.gamesetup.enabletips", enabled, "config/user.cfg"); } function verticallyDistributeGUIObjects(parent, objectHeight, ignore) { let yPos; let parentObject = Engine.GetGUIObjectByName(parent); for (let child of parentObject.children) { if (ignore.indexOf(child.name) != -1) continue; let childSize = child.size; yPos = yPos || childSize.top; if (child.hidden) continue; childSize.top = yPos; childSize.bottom = yPos + objectHeight - 2; child.size = childSize; yPos += objectHeight; } return yPos; } /** * Remove empty space in case of hidden options (like cheats, rating or victory duration) */ function resizeMoreOptionsWindow() { verticallyDistributeGUIObjects("mapOptions", 32, []); let yPos = verticallyDistributeGUIObjects("moreOptions", 32, ["moreOptionsLabel"]); // Resize the vertically centered window containing the options let moreOptions = Engine.GetGUIObjectByName("moreOptions"); let mSize = moreOptions.size; mSize.bottom = mSize.top + yPos + 20; moreOptions.size = mSize; } /** * Called when the client disconnects. * The other cases from NetClient should never occur in the gamesetup. */ function handleNetStatusMessage(message) { if (message.status != "disconnected") { error("Unrecognised netstatus type " + message.status); return; } cancelSetup(); reportDisconnect(message.reason, true); } /** * Called whenever a client clicks on ready (or not ready). */ function handleReadyMessage(message) { --g_ReadyChanged; if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1) addChatMessage({ "type": "ready", "status": message.status, "guid": message.guid }); g_PlayerAssignments[message.guid].status = message.status; updateGUIObjects(); } /** * Called after every player is ready and the host decided to finally start the game. */ function handleGamestartMessage(message) { // Immediately inform the lobby server instead of waiting for the load to finish if (g_IsController && Engine.HasXmppClient()) { sendRegisterGameStanzaImmediate(); let clients = formatClientsForStanza(); Engine.SendChangeStateGame(clients.connectedPlayers, clients.list); } Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked" : g_IsNetworked, "playerAssignments": g_PlayerAssignments, "isController": g_IsController }); } /** * Called whenever the host changed any setting. */ function handleGamesetupMessage(message) { if (!message.data) return; g_GameAttributes = message.data; if (!!g_GameAttributes.settings.RatingEnabled) { g_GameAttributes.settings.CheatsEnabled = false; g_GameAttributes.settings.LockTeams = true; g_GameAttributes.settings.LastManStanding = false; } Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled); resetReadyData(); updateGUIObjects(); hideLoadingWindow(); } /** * Called whenever a client joins/leaves or any gamesetting is changed. */ function handlePlayerAssignmentMessage(message) { let playerChange = false; for (let guid in message.newAssignments) if (!g_PlayerAssignments[guid]) { onClientJoin(guid, message.newAssignments); playerChange = true; } for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) { onClientLeave(guid); playerChange = true; } g_PlayerAssignments = message.newAssignments; sanitizePlayerData(g_GameAttributes.settings.PlayerData); updateGUIObjects(); if (playerChange) sendRegisterGameStanzaImmediate(); else sendRegisterGameStanza(); } function onClientJoin(newGUID, newAssignments) { let playername = newAssignments[newGUID].name; addChatMessage({ "type": "connect", "guid": newGUID, "username": playername }); let isRejoiningPlayer = newAssignments[newGUID].player != -1; // Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name) { let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers"); if (assignOption == "disabled" || assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername)[0]) == -1) return; } let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v,i) => Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i+1) ); // Client is not and cannot become assigned as player if (!isRejoiningPlayer && freeSlot == -1) return; // Assign the joining client to the free slot if (g_IsController && !isRejoiningPlayer) Engine.AssignNetworkPlayer(freeSlot + 1, newGUID); resetReadyData(); } function onClientLeave(guid) { addChatMessage({ "type": "disconnect", "guid": guid }); if (g_PlayerAssignments[guid].player != -1) resetReadyData(); } /** * Doesn't translate, so that lobby clients can do that locally * (even if they don't have that map). */ function getMapDisplayName(map) { if (map == "random") return map; let mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Name) return map; return mapData.settings.Name; } function getMapPreview(map) { let mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Preview) return "nopreview.png"; return mapData.settings.Preview; } /** * Filter maps with filterFunc and by chosen map type. * * @param {function} filterFunc - Filter function that should be applied. * @return {Array} the maps that match the filterFunc and the chosen map type. */ function getFilteredMaps(filterFunc) { if (!g_MapPath[g_GameAttributes.mapType]) { error("Unexpected map type: " + g_GameAttributes.mapType); return []; } let mapFiles = g_GameAttributes.mapType == "random" ? getJSONFileList(g_GameAttributes.mapPath) : getXMLFileList(g_GameAttributes.mapPath); let maps = []; // TODO: Should verify these are valid maps before adding to list for (let mapFile of mapFiles) { let file = g_GameAttributes.mapPath + mapFile; let mapData = loadMapData(file); if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || [])) continue; maps.push({ "file": file, "name": translate(getMapDisplayName(file)), "color": g_ColorRegular, "description": translate(mapData.settings.Description) }); } return maps; } /** * Initialize the dropdown containing all map filters for the selected maptype. */ function reloadMapFilterList() { g_MapFilterList = prepareForDropdown(g_MapFilters.filter( mapFilter => getFilteredMaps(mapFilter.filter).length )); initDropdown("mapFilter"); reloadMapList(); } /** * Initialize the dropdown containing all maps for the selected maptype and mapfilter. */ function reloadMapList() { let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter); let filterFunc = g_MapFilterList.filter[filterID]; let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase); if (g_GameAttributes.mapType == "random") mapList.unshift({ "file": "random", "name": translateWithContext("map selection", "Random"), "color": g_ColorRandom, "description": translate("Pick any of the given maps at random.") }); g_MapSelectionList = prepareForDropdown(mapList); initDropdown("mapSelection"); } function loadMapData(name) { if (!name || !g_MapPath[g_GameAttributes.mapType]) return undefined; if (name == "random") return { "settings": { "Name": "", "Description": "" } }; if (!g_MapData[name]) g_MapData[name] = g_GameAttributes.mapType == "random" ? Engine.ReadJSONFile(name + ".json") : Engine.LoadMapSettings(name); return g_MapData[name]; } /** * Sets the gameattributes the way they were the last time the user left the gamesetup. */ function loadPersistMatchSettings() { if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial) return; let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP; if (!Engine.FileExists(settingsFile)) return; let attrs = Engine.ReadJSONFile(settingsFile); if (!attrs || !attrs.settings) return; g_IsInGuiUpdate = true; let mapName = attrs.map || ""; let mapSettings = attrs.settings; g_GameAttributes = attrs; if (!g_IsNetworked) mapSettings.CheatsEnabled = true; // Replace unselectable civs with random civ let playerData = mapSettings.PlayerData; if (playerData && g_GameAttributes.mapType != "scenario") for (let i in playerData) if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup) playerData[i].Civ = "random"; // Apply map settings let newMapData = loadMapData(mapName); if (newMapData && newMapData.settings) { for (let prop in newMapData.settings) mapSettings[prop] = newMapData.settings[prop]; if (playerData) mapSettings.PlayerData = playerData; } if (mapSettings.PlayerData) sanitizePlayerData(mapSettings.PlayerData); // Reload, as the maptype or mapfilter might have changed reloadMapFilterList(); g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient(); Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled); supplementDefaults(); g_IsInGuiUpdate = false; } function savePersistMatchSettings() { if (g_IsTutorial) return; let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {}; Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes); } function sanitizePlayerData(playerData) { // Remove gaia if (playerData.length && !playerData[0]) playerData.shift(); playerData.forEach((pData, index) => { // Use defaults if the map doesn't specify a value for (let prop in g_DefaultPlayerData[index]) if (!(prop in pData)) pData[prop] = g_DefaultPlayerData[index][prop]; // Replace colors with the best matching color of PlayerDefaults if (g_GameAttributes.mapType != "scenario") { let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color)); let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance))); pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance); } // If there is a player in that slot, then there can't be an AI if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1)) pData.AI = ""; }); ensureUniquePlayerColors(playerData); } function cancelSetup() { if (g_IsController) savePersistMatchSettings(); Engine.DisconnectNetworkGame(); if (Engine.HasXmppClient()) { Engine.LobbySetPlayerPresence("available"); if (g_IsController) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_lobby.xml"); } else Engine.SwitchGuiPage("page_pregame.xml"); } /** * Can't init the GUI before the first tick. * Process netmessages afterwards. */ function onTick() { if (!g_Settings) return; // First tick happens before first render, so don't load yet if (g_LoadingState == 0) ++g_LoadingState; else if (g_LoadingState == 1) { initGUIObjects(); ++g_LoadingState; } else if (g_LoadingState == 2) handleNetMessages(); updateTimers(); } /** * Handles all pending messages sent by the net client. */ function handleNetMessages() { while (g_IsNetworked) { let message = Engine.PollNetworkClient(); if (!message) break; log("Net message: " + uneval(message)); if (g_NetMessageTypes[message.type]) g_NetMessageTypes[message.type](message); else error("Unrecognised net message type " + message.type); } } /** * Called when the map or the number of players changes. */ function unassignInvalidPlayers(maxPlayers) { if (g_IsNetworked) { // Remove invalid playerIDs from the servers playerassignments copy for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID) Engine.AssignNetworkPlayer(playerID, ""); } else if (g_PlayerAssignments.local.player > maxPlayers) g_PlayerAssignments.local.player = -1; } function ensureUniquePlayerColors(playerData) { for (let i = playerData.length - 1; i >= 0; --i) // If someone else has that color, assign an unused color if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color))) playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color))); } function selectMap(name) { // Reset some map specific properties which are not necessarily redefined on each map for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates"]) g_GameAttributes.settings[prop] = undefined; let mapData = loadMapData(name); let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {}; // Reset victory conditions if (g_GameAttributes.mapType != "random") { let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default; g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx]; g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx]; } if (g_GameAttributes.mapType == "scenario") { delete g_GameAttributes.settings.VictoryDuration; delete g_GameAttributes.settings.LastManStanding; delete g_GameAttributes.settings.RegicideGarrison; } if (mapSettings.PlayerData) sanitizePlayerData(mapSettings.PlayerData); // Copy any new settings g_GameAttributes.map = name; g_GameAttributes.script = mapSettings.Script; if (g_GameAttributes.map !== "random") for (let prop in mapSettings) g_GameAttributes.settings[prop] = mapSettings[prop]; unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length); supplementDefaults(); } function isControlArrayElementHidden(idx) { return idx !== undefined && idx >= g_GameAttributes.settings.PlayerData.length; } /** * @param idx - Only specified for dropdown arrays. */ function updateGUIDropdown(name, idx = undefined) { let [guiName, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = idx === undefined ? "": "[" + idx + "]"; let dropdown = Engine.GetGUIObjectByName(guiName + guiIdx + idxName); let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName); let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName); let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName); let indexHidden = isControlArrayElementHidden(idx); let obj = (idx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let selected = indexHidden ? -1 : dropdown.list_data.indexOf(String(obj.get(idx))); let enabled = !indexHidden && (!obj.enabled || obj.enabled(idx)); let hidden = indexHidden || obj.hidden && obj.hidden(idx); dropdown.hidden = !g_IsController || !enabled || hidden; dropdown.selected = selected; dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, idx) : ""; if (frame) frame.hidden = hidden; if (title && obj.title && !indexHidden) title.caption = sprintf(translate("%(option)s:"), { "option": obj.title(idx) }); if (label && !indexHidden) { label.hidden = g_IsController && enabled || hidden; label.caption = selected == -1 ? translateWithContext("option value", "Unknown") : dropdown.list[selected]; } } /** * Not used for the player assignments, so playerCheckboxes are not implemented, * hence no index. */ function updateGUICheckbox(name) { let obj = g_Checkboxes[name]; let checked = obj.get(); let hidden = obj.hidden && obj.hidden(); let enabled = !obj.enabled || obj.enabled(); let [guiName, guiIdx] = getGUIObjectNameFromSetting(name); let checkbox = Engine.GetGUIObjectByName(guiName + guiIdx); let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx); let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx); let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx); checkbox.checked = checked; checkbox.enabled = enabled; checkbox.hidden = hidden || !g_IsController; checkbox.tooltip = obj.tooltip ? obj.tooltip() : ""; label.caption = checked ? translate("Yes") : translate("No"); label.hidden = hidden || g_IsController; if (frame) frame.hidden = hidden; if (title && obj.title) title.caption = sprintf(translate("%(option)s:"), { "option": obj.title() }); } function updateGUIMiscControl(name, idx) { let idxName = idx === undefined ? "": "[" + idx + "]"; let obj = (idx === undefined ? g_MiscControls : g_PlayerMiscControls)[name]; let control = Engine.GetGUIObjectByName(name + idxName); if (!control) warn("No GUI object with name '" + name + "'"); let hide = isControlArrayElementHidden(idx); control.hidden = hide; if (hide) return; for (let property in obj) control[property] = obj[property](idx); } function launchGame() { if (!g_IsController) { error("Only host can start game"); return; } if (!g_GameAttributes.map) return; savePersistMatchSettings(); // Select random map if (g_GameAttributes.map == "random") { let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts; let gameTypeSelected = g_GameAttributes.settings.GameType; selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1))); g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected; g_GameAttributes.settings.GameType = gameTypeSelected; } g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []); // Prevent reseting the readystate g_GameStarted = true; g_GameAttributes.settings.mapType = g_GameAttributes.mapType; // Get a unique array of selectable cultures let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture); cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index); // Determine random civs and botnames for (let i in g_GameAttributes.settings.PlayerData) { // Pick a random civ of a random culture let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random"; if (chosenCiv == "random") { let culture = pickRandom(cultures); chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture)); } g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv; // Pick one of the available botnames for the chosen civ if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI) continue; let chosenName = pickRandom(g_CivData[chosenCiv].AINames); if (!g_IsNetworked) chosenName = translate(chosenName); // Count how many players use the chosenName let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length; g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName : sprintf(translate("%(playerName)s %(romanNumber)s"), { "playerName": chosenName, "romanNumber": g_RomanNumbers[usedName+1] }); } // Copy playernames for the purpose of replays for (let guid in g_PlayerAssignments) { let player = g_PlayerAssignments[guid]; if (player.player > 0) // not observer or GAIA g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name; } // Seed used for both map generation and simulation g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32)); g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32)); // Used for identifying rated game reports for the lobby g_GameAttributes.matchID = Engine.GetMatchID(); if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); Engine.StartNetworkGame(); } else { // Find the player ID which the user has been assigned to let playerID = -1; for (let i in g_GameAttributes.settings.PlayerData) { let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]"); if (assignBox.list_data[assignBox.selected] == "guid:local") playerID = +i+1; } Engine.StartGame(g_GameAttributes, playerID); Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked": g_IsNetworked, "playerAssignments": g_PlayerAssignments }); } } function launchTutorial() { g_GameAttributes.mapType = "scenario"; selectMap("maps/tutorials/starting_economy_walkthrough"); launchGame(); } /** * Don't set any attributes here, just show the changes in the GUI. * * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously. */ function updateGUIObjects() { g_IsInGuiUpdate = true; reloadMapFilterList(); updatePlayerAssignmentChoices(); // Hide exceeding dropdowns and checkboxes for (let panel in g_OptionOrderGUI) for (let child of Engine.GetGUIObjectByName(panel + "Options").children) child.hidden = true; // Show the relevant ones for (let name in g_Dropdowns) updateGUIDropdown(name); for (let name in g_Checkboxes) updateGUICheckbox(name); for (let i = 0; i < g_MaxPlayers; ++i) { for (let name in g_PlayerDropdowns) updateGUIDropdown(name, i); for (let name in g_PlayerMiscControls) updateGUIMiscControl(name, i); } for (let name in g_MiscControls) updateGUIMiscControl(name); updateGameDescription(); resizeMoreOptionsWindow(); rightAlignCancelButton(); updateAutocompleteEntries(); g_IsInGuiUpdate = false; // Refresh AI config page if (g_LastViewedAIPlayer != -1) { Engine.PopGuiPage(); openAIConfig(g_LastViewedAIPlayer); } } function rightAlignCancelButton() { let offset = 10; let startGame = Engine.GetGUIObjectByName("startGame"); let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset; let cancelGame = Engine.GetGUIObjectByName("cancelGame"); let cancelGameSize = cancelGame.size; let buttonWidth = cancelGameSize.right - cancelGameSize.left; cancelGameSize.right = right; right -= buttonWidth; for (let element of ["cheatWarningText", "onscreenToolTip"]) { let elementSize = Engine.GetGUIObjectByName(element).size; elementSize.right = right - (cancelGameSize.left - elementSize.right); Engine.GetGUIObjectByName(element).size = elementSize; } cancelGameSize.left = right; cancelGame.size = cancelGameSize; } function updateGameDescription() { setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map)); Engine.GetGUIObjectByName("mapInfoName").caption = translateMapTitle(getMapDisplayName(g_GameAttributes.map)); Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription(); } /** * Broadcast the changed settings to all clients and the lobbybot. */ function updateGameAttributes() { if (g_IsInGuiUpdate || !g_IsController) return; if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); if (g_LoadingState >= 2) sendRegisterGameStanza(); resetReadyData(); } else updateGUIObjects(); } function openAIConfig(playerSlot) { g_LastViewedAIPlayer = playerSlot; Engine.PushGuiPage("page_aiconfig.xml", { "callback": "AIConfigCallback", "isController": g_IsController, "playerSlot": playerSlot, "id": g_GameAttributes.settings.PlayerData[playerSlot].AI, "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff }); } /** * Called after closing the dialog. */ function AIConfigCallback(ai) { g_LastViewedAIPlayer = -1; if (!ai.save || !g_IsController) return; g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty; updateGameAttributes(); } function updatePlayerAssignmentChoices() { let playerChoices = sortGUIDsByPlayerID().map(guid => ({ "Choice": "guid:" + guid, "Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player, "Name": g_PlayerAssignments[guid].name })); // Only display hidden AIs if the map preselects them let aiChoices = g_Settings.AIDescriptions .filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id)) .map(ai => ({ "Choice": "ai:" + ai.id, "Name": sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }), "Color": g_PlayerAssignmentColors.AI })); let unassignedSlot = [{ "Choice": "unassigned", "Name": translate("Unassigned"), "Color": g_PlayerAssignmentColors.unassigned }]; g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot)); initPlayerDropdowns("playerAssignment"); } function swapPlayers(guidToSwap, newSlot) { // Player slots are indexed from 0 as Gaia is omitted. let newPlayerID = newSlot + 1; let playerID = g_PlayerAssignments[guidToSwap].player; // Attempt to swap the player or AI occupying the target slot, // if any, into the slot this player is currently in. if (playerID != -1) { for (let guid in g_PlayerAssignments) { // Move the player in the destination slot into the current slot. if (g_PlayerAssignments[guid].player != newPlayerID) continue; if (g_IsNetworked) Engine.AssignNetworkPlayer(playerID, guid); else g_PlayerAssignments[guid].player = playerID; break; } // Transfer the AI from the target slot to the current slot. g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI; g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff; // Swap civilizations and colors if they aren't fixed if (g_GameAttributes.mapType != "scenario") { [g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] = [g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ]; [g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] = [g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color]; } } if (g_IsNetworked) Engine.AssignNetworkPlayer(newPlayerID, guidToSwap); else g_PlayerAssignments[guidToSwap].player = newPlayerID; g_GameAttributes.settings.PlayerData[newSlot].AI = ""; } function submitChatInput() { let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; if (!text.length) return; input.caption = ""; if (executeNetworkCommand(text)) return; Engine.SendNetworkChat(text); } function senderFont(text) { return '[font="' + g_SenderFont + '"]' + text + '[/font]'; } function systemMessage(message) { return senderFont(sprintf(translate("== %(message)s"), { "message": message })); } function colorizePlayernameByGUID(guid, username = "") { // TODO: Maybe the host should have the moderator-prefix? if (!username) username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player"); let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; let color = g_ColorRegular; if (playerID > 0) { color = g_GameAttributes.settings.PlayerData[playerID - 1].Color; // Enlighten playercolor to improve readability let [h, s, l] = rgbToHsl(color.r, color.g, color.b); let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l)); color = rgbToGuiColor({ "r": r, "g": g, "b": b }); } return '[color="'+ color +'"]' + username + '[/color]'; } function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; if (msg.type == "chat") { let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name; if (userName != g_PlayerAssignments[msg.guid].name) notifyUser(userName, msg.text); } let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || ""); let text = g_FormatChatMessage[msg.type](msg, user); if (!text) return; if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true") text = sprintf(translate("%(time)s %(message)s"), { "time": sprintf(translate("\\[%(time)s]"), { "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm")) }), "message": text }); g_ChatMessages.push(text); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } function showMoreOptions(show) { Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show; Engine.GetGUIObjectByName("moreOptions").hidden = !show; } function resetCivilizations() { for (let i in g_GameAttributes.settings.PlayerData) g_GameAttributes.settings.PlayerData[i].Civ = "random"; updateGameAttributes(); } function resetTeams() { for (let i in g_GameAttributes.settings.PlayerData) g_GameAttributes.settings.PlayerData[i].Team = -1; updateGameAttributes(); } function toggleReady() { setReady((g_IsReady + 1) % 3, true); } function setReady(ready, sendMessage) { g_IsReady = ready; if (sendMessage) Engine.SendNetworkReady(g_IsReady); updateGUIObjects(); } function resetReadyData() { if (g_GameStarted) return; if (g_ReadyChanged < 1) addChatMessage({ "type": "settings" }); else if (g_ReadyChanged == 2 && !g_ReadyInit) return; // duplicate calls on init else g_ReadyInit = false; g_ReadyChanged = 2; if (!g_IsNetworked) g_IsReady = 2; else if (g_IsController) { Engine.ClearAllPlayerReady(); setReady(2, true); } else if (g_IsReady != 2) setReady(0, false); } /** * Send a list of playernames and distinct between players and observers. * Don't send teams, AIs or anything else until the game was started. * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. */ function formatClientsForStanza() { let connectedPlayers = 0; let playerData = []; for (let guid in g_PlayerAssignments) { let pData = { "Name": g_PlayerAssignments[guid].name }; if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1]) ++connectedPlayers; else pData.Team = "observer"; playerData.push(pData); } return { "list": playerDataToStringifiedTeamList(playerData), "connectedPlayers": connectedPlayers }; } /** * Send the relevant gamesettings to the lobbybot immediately. */ function sendRegisterGameStanzaImmediate() { if (!g_IsController || !Engine.HasXmppClient()) return; if (g_GameStanzaTimer !== undefined) { clearTimeout(g_GameStanzaTimer); g_GameStanzaTimer = undefined; } let clients = formatClientsForStanza(); let stanza = { "name": g_ServerName, "port": g_ServerPort, "hostUsername": g_Username, "mapName": g_GameAttributes.map, "niceMapName": getMapDisplayName(g_GameAttributes.map), "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default", "mapType": g_GameAttributes.mapType, "victoryCondition": g_GameAttributes.settings.GameType, "nbp": clients.connectedPlayers, "maxnbp": g_GameAttributes.settings.PlayerData.length, "players": clients.list, "stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "", "stunPort": g_StunEndpoint ? g_StunEndpoint.port : "", }; // Only send the stanza if the relevant settings actually changed if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop])) return; g_LastGameStanza = stanza; Engine.SendRegisterGame(stanza); } /** * Send the relevant gamesettings to the lobbybot in a deferred manner. */ function sendRegisterGameStanza() { if (!g_IsController || !Engine.HasXmppClient()) return; if (g_GameStanzaTimer !== undefined) clearTimeout(g_GameStanzaTimer); g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000); } /** * Figures out all strings that can be autocompleted and sorts * them by priority (so that playernames are always autocompleted first). */ function updateAutocompleteEntries() { let autocomplete = { "0": [] }; for (let control of [g_Dropdowns, g_Checkboxes]) for (let name in control) autocomplete[0] = autocomplete[0].concat(control[name].title()); for (let dropdown of [g_Dropdowns, g_PlayerDropdowns]) for (let name in dropdown) { let priority = dropdown[name].autocomplete; if (priority === undefined) continue; autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels()); } g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []); } Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 20100) @@ -1,1273 +1,1270 @@ /** * All known cheat commands. - * @type {Object} */ const g_Cheats = getCheatsData(); /** * Number of seconds after which chatmessages will disappear. */ var g_ChatTimeout = 30; /** * Maximum number of lines to display simultaneously. */ var g_ChatLines = 20; /** * The currently displayed strings, limited by the given timeframe and limit above. */ var g_ChatMessages = []; /** * All unparsed chat messages received since connect, including timestamp. */ var g_ChatHistory = []; /** * Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds. */ var g_ChatTimers = []; /** * Command to send to the previously selected private chat partner. */ var g_LastChatAddressee = ""; /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "out-of-sync": msg => { onNetworkOutOfSync(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { setClientPauseState(msg.guid, msg.pause); }, "clients-loading": msg => { handleClientsLoadingMessage(msg.guids); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "chat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }); }, "aichat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }); }, "gamesetup": msg => {}, // Needed for autostart "start": msg => {} }; var g_FormatChatMessage = { "system": msg => msg.text, "connect": msg => sprintf( g_PlayerAssignments[msg.guid].player != -1 ? // Translation: A player that left the game joins again translate("%(player)s is starting to rejoin the game.") : // Translation: A player joins the game for the first time translate("%(player)s is starting to join the game."), { "player": colorizePlayernameByGUID(msg.guid) } ), "disconnect": msg => sprintf(translate("%(player)s has left the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "rejoined": msg => sprintf( g_PlayerAssignments[msg.guid].player != -1 ? // Translation: A player that left the game joins again translate("%(player)s has rejoined the game.") : // Translation: A player joins the game for the first time translate("%(player)s has joined the game."), { "player": colorizePlayernameByGUID(msg.guid) } ), "kicked": msg => sprintf( msg.banned ? translate("%(username)s has been banned") : translate("%(username)s has been kicked"), { "username": colorizePlayernameHelper( msg.username, g_Players.findIndex(p => p.name == msg.username) ) } ), "clientlist": msg => getUsernameList(), "message": msg => formatChatCommand(msg), "defeat-victory": msg => formatDefeatVictoryMessage(msg.message, msg.players), "diplomacy": msg => formatDiplomacyMessage(msg), "tribute": msg => formatTributeMessage(msg), "barter": msg => formatBarterMessage(msg), "attack": msg => formatAttackMessage(msg), "phase": msg => formatPhaseMessage(msg) }; /** * Show a label and grey overlay or hide both on connection change. */ var g_StatusMessageTypes = { "authenticated": msg => translate("Connection to the server has been authenticated."), "connected": msg => translate("Connected to the server."), "disconnected": msg => translate("Connection to the server has been lost.") + "\n" + // Translation: States the reason why the client disconnected from the server. sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(msg.reason, true) }), "waiting_for_players": msg => translate("Waiting for players to connect:"), "join_syncing": msg => translate("Synchronising gameplay with other players..."), "active": msg => "" }; /** * Chatmessage shown after commands like /me or /enemies. */ var g_ChatCommands = { "regular": { "context": translate("(%(context)s) %(userTag)s %(message)s"), "no-context": translate("%(userTag)s %(message)s") }, "me": { "context": translate("(%(context)s) * %(user)s %(message)s"), "no-context": translate("* %(user)s %(message)s") } }; var g_ChatAddresseeContext = { "/team": translate("Team"), "/allies": translate("Ally"), "/enemies": translate("Enemy"), "/observers": translate("Observer"), "/msg": translate("Private") }; /** * Returns true if the current player is an addressee, given the chat message type and sender. */ var g_IsChatAddressee = { "/team": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1 && g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team, "/allies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isMutualAlly[Engine.GetPlayerID()], "/enemies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isEnemy[Engine.GetPlayerID()], "/observers": senderID => g_IsObserver, "/msg": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID() }; /** * Notice only messages will be filtered that are visible to the player in the first place. */ var g_ChatHistoryFilters = [ { "key": "all", "text": translateWithContext("chat history filter", "Chat and notifications"), "filter": (msg, senderID) => true }, { "key": "chat", "text": translateWithContext("chat history filter", "Chat messages"), "filter": (msg, senderID) => msg.type == "message" }, { "key": "player", "text": translateWithContext("chat history filter", "Players chat"), "filter": (msg, senderID) => msg.type == "message" && senderID > 0 && !isPlayerObserver(senderID) }, { "key": "ally", "text": translateWithContext("chat history filter", "Ally chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/allies" }, { "key": "enemy", "text": translateWithContext("chat history filter", "Enemy chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/enemies" }, { "key": "observer", "text": translateWithContext("chat history filter", "Observer chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/observers" }, { "key": "private", "text": translateWithContext("chat history filter", "Private chat"), "filter": (msg, senderID) => !!msg.isVisiblePM } ]; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Chatmessage shown on diplomacy change. */ var g_DiplomacyMessages = { "active": { "ally": translate("You are now allied with %(player)s."), "enemy": translate("You are now at war with %(player)s."), "neutral": translate("You are now neutral with %(player)s.") }, "passive": { "ally": translate("%(player)s is now allied with you."), "enemy": translate("%(player)s is now at war with you."), "neutral": translate("%(player)s is now neutral with you.") }, "observer": { "ally": translate("%(player)s is now allied with %(player2)s."), "enemy": translate("%(player)s is now at war with %(player2)s."), "neutral": translate("%(player)s is now neutral with %(player2)s.") } }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ var g_NotificationsTypes = { "chat": function(notification, player) { let message = { "type": "message", "guid": findGuidForPlayerID(player) || -1, "text": notification.message }; if (message.guid == -1) message.player = player; addChatMessage(message); }, "aichat": function(notification, player) { let message = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); updatePlayerData(); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); }, "tutorial": function(notification, player) { updateTutorial(notification); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, "amountsSold": notification.amountsSold, "amountsBought": notification.amountsBought, "resourceSold": notification.resourceSold, "resourceBought": notification.resourceBought }); }, "spy-response": function(notification, player) { if (g_ViewedPlayer == player) setCameraFollow(notification.entity); }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "phase": function(notification, player) { addChatMessage({ "type": "phase", "player": player, "phaseName": notification.phaseName, "phaseState": notification.phaseState }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "resetselectionpannel": function(notification, player) { if (player != Engine.GetPlayerID()) return; g_Selection.rebuildSelection({}); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore boring animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; // Focus the building to construct if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } else if (cmd.type == "delete-entities" && notification.position) { Engine.CameraMoveTo(notification.position.x, notification.position.y); } // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1) DrawTargetMarker(cmd); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); } }; /** * Loads all known cheat commands. - * - * @returns {Object} */ function getCheatsData() { let cheats = {}; for (let fileName of getJSONFileList("simulation/data/cheats/")) { let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json"); if (!currentCheat) continue; if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1) warn("Cheat name '" + currentCheat.Name + "' is already present"); else cheats[currentCheat.Name] = currentCheat.Data; } - return cheats; + return deepfreeze(cheats); } /** * Reads userinput from the chat and sends a simulation command in case it is a known cheat. * * @returns {boolean} - True if a cheat was executed. */ function executeCheat(text) { if (!controlsPlayer(Engine.GetPlayerID()) || !g_Players[Engine.GetPlayerID()].cheatsEnabled) return false; // Find the cheat code that is a prefix of the user input let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0); if (!cheatCode) return false; let cheat = g_Cheats[cheatCode]; let parameter = text.substr(cheatCode.length); if (cheat.isNumeric) parameter = +parameter; if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0)) parameter = cheat.DefaultParameter; Engine.PostNetworkCommand({ "type": "cheat", "action": cheat.Action, "text": cheat.Type, "player": Engine.GetPlayerID(), "parameter": parameter, "templates": cheat.Templates, "selected": g_Selection.toList() }); return true; } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } /** * Updates the tutorial panel when a new goal. */ function updateTutorial(notification) { // Show the tutorial panel if not yet done Engine.GetGUIObjectByName("tutorialPanel").hidden = false; if (notification.warning) { Engine.GetGUIObjectByName("tutorialWarning").caption = '[color="orange"]' + translate(notification.warning) + '[/color]'; return; } let tutorialText = Engine.GetGUIObjectByName("tutorialText"); tutorialText.caption = tutorialText.caption.replace('[color="yellow"]', '').replace('[/color]', '') + (tutorialText.caption ? "\n" : "" ) + '[color="yellow"]' + notification.instructions.reduce((instructions, item) => instructions + translate(item), "") + '[/color]'; if (notification.readyButton) { Engine.GetGUIObjectByName("tutorialReady").hidden = false; if (notification.leave) { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial."); Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit"); Engine.GetGUIObjectByName("tutorialReady").onPress = leaveGame; } else Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready."); } else { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions."); Engine.GetGUIObjectByName("tutorialReady").hidden = true; } } /** * Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc. */ function updateTimeNotifications() { let notifications = Engine.GuiInterfaceCall("GetTimeNotifications", g_ViewedPlayer); let notificationText = ""; for (let n of notifications) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); parameters.time = timeToString(n.endTime - GetSimState().timeElapsed); colorizePlayernameParameters(parameters); notificationText += sprintf(message, parameters) + "\n"; } Engine.GetGUIObjectByName("notificationText").caption = notificationText; } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } } /** * @param {Object} message */ function handleNetStatusMessage(message) { if (g_Disconnected) return; if (!g_StatusMessageTypes[message.status]) { error("Unrecognised netstatus type '" + message.status + "'"); return; } g_IsNetworkedActive = message.status == "active"; let netStatus = Engine.GetGUIObjectByName("netStatus"); let statusMessage = g_StatusMessageTypes[message.status](message); netStatus.caption = statusMessage; netStatus.hidden = !statusMessage; let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText"); loadingClientsText.hidden = message.status != "waiting_for_players"; if (message.status == "disconnected") { // Hide the pause overlay, and pause animations. Engine.GetGUIObjectByName("pauseOverlay").hidden = true; Engine.SetPaused(true, false); g_Disconnected = true; updateCinemaPath(); closeOpenDialogs(); } } function handleClientsLoadingMessage(guids) { let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText"); loadingClientsText.caption = guids.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of client loading messages", ", ")); } function onNetworkOutOfSync(msg) { let txt = [ sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { "turn": msg.turn }), sprintf(translateWithContext("Out-Of-Sync", "Players: %(players)s"), { "players": msg.players.join(translateWithContext("Separator for a list of players", ", ")) }), msg.hash == msg.expectedHash ? translateWithContext("Out-Of-Sync", "Your game state is identical to the hosts game state.") : translateWithContext("Out-Of-Sync", "Your game state differs from the hosts game state."), "" ]; if (msg.turn > 1 && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.AI)) txt.push(translateWithContext("Out-Of-Sync", "Rejoining Multiplayer games with AIs is not supported yet!")); else { txt.push( translateWithContext("Out-Of-Sync", "Ensure all players use the same mods."), translateWithContext("Out-Of-Sync", 'Click on "Report a Bug" in the main menu to help fix this.'), sprintf(translateWithContext("Out-Of-Sync", "Replay saved to %(filepath)s"), { "filepath": escapeText(msg.path_replay) }), sprintf(translateWithContext("Out-Of-Sync", "Dumping current state to %(filepath)s"), { "filepath": escapeText(msg.path_oos_dump) }) ); } messageBox( 600, 280, txt.join("\n"), translate("Out of Sync") ); } function onReplayOutOfSync() { messageBox( 500, 140, translate("Out-Of-Sync error!") + "\n" + // Translation: This is shown if replay is out of sync translateWithContext("Out-Of-Sync", "The current game state is different from the original game state."), translate("Out of Sync") ); } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); updateGUIObjects(); updateChatAddressees(); sendLobbyPlayerlistUpdate(); } function onClientJoin(guid) { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); } function onClientLeave(guid) { setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function updateChatAddressees() { // Remember previously selected item let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); let selectedName = chatAddressee.list_data[chatAddressee.selected] || ""; selectedName = selectedName.substr(0, 4) == "/msg" && selectedName.substr(5); let addressees = [ { "label": translateWithContext("chat addressee", "Everyone"), "cmd": "" } ]; if (!g_IsObserver) { addressees.push({ "label": translateWithContext("chat addressee", "Allies"), "cmd": "/allies" }); addressees.push({ "label": translateWithContext("chat addressee", "Enemies"), "cmd": "/enemies" }); } addressees.push({ "label": translateWithContext("chat addressee", "Observers"), "cmd": "/observers" }); // Add playernames for private messages let guids = sortGUIDsByPlayerID(); for (let guid of guids) { if (guid == Engine.GetPlayerGUID()) continue; let playerID = g_PlayerAssignments[guid].player; // Don't provide option for PM from observer to player if (g_IsObserver && !isPlayerObserver(playerID)) continue; let colorBox = isPlayerObserver(playerID) ? "" : colorizePlayernameHelper("■", playerID) + " "; addressees.push({ "cmd": "/msg " + g_PlayerAssignments[guid].name, "label": colorBox + g_PlayerAssignments[guid].name }); } // Select mock item if the selected addressee went offline if (selectedName && guids.every(guid => g_PlayerAssignments[guid].name != selectedName)) addressees.push({ "cmd": "/msg " + selectedName, "label": sprintf(translate("\\[OFFLINE] %(player)s"), { "player": selectedName }) }); let oldChatAddressee = chatAddressee.list_data[chatAddressee.selected]; chatAddressee.list = addressees.map(adressee => adressee.label); chatAddressee.list_data = addressees.map(adressee => adressee.cmd); chatAddressee.selected = Math.max(0, chatAddressee.list_data.indexOf(oldChatAddressee)); } /** * Send text as chat. Don't look for commands. * * @param {string} text */ function submitChatDirectly(text) { if (!text.length) return; if (g_IsNetworked) Engine.SendNetworkChat(text); else addChatMessage({ "type": "message", "guid": "local", "text": text }); } /** * Loads the text from the GUI window, checks if it is a local command * or cheat and executes it. Otherwise sends it as chat. */ function submitChatInput() { let text = Engine.GetGUIObjectByName("chatInput").caption; closeChat(); if (!text.length) return; if (executeNetworkCommand(text)) return; if (executeCheat(text)) return; let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0)) text = chatAddressee.list_data[chatAddressee.selected] + " " + text; let selectedChat = chatAddressee.list_data[chatAddressee.selected]; if (selectedChat.startsWith("/msg")) g_LastChatAddressee = selectedChat; submitChatDirectly(text); } /** * Displays the prepared chatmessage. * * @param msg {Object} */ function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; let formatted = g_FormatChatMessage[msg.type](msg); if (!formatted) return; // Update chat overlay g_ChatMessages.push(formatted); g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000)); if (g_ChatMessages.length > g_ChatLines) removeOldChatMessage(); else Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); // Save to chat history let historical = { "txt": formatted, "timePrefix": sprintf(translate("\\[%(time)s]"), { "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm")) }), "filter": {} }; // Apply the filters now before diplomacies or playerstates change let senderID = msg.guid && g_PlayerAssignments[msg.guid] ? g_PlayerAssignments[msg.guid].player : 0; for (let filter of g_ChatHistoryFilters) historical.filter[filter.key] = filter.filter(msg, senderID); g_ChatHistory.push(historical); updateChatHistory(); } /** * Called when the timer has run out for the oldest chatmessage or when the message limit is reached. */ function removeOldChatMessage() { clearTimeout(g_ChatTimers[0]); g_ChatTimers.shift(); g_ChatMessages.shift(); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white"; return '[color="' + playerColor + '"]' + (username || translate("Unknown Player")) + "[/color]"; } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } function formatDefeatVictoryMessage(message, players) { if (!message.pluralMessage) return sprintf(translate(message), { "player": colorizePlayernameByID(players[0]) }); let mPlayers = players.map(playerID => colorizePlayernameByID(playerID)); let lastPlayer = mPlayers.pop(); return sprintf(translatePlural(message.message, message.pluralMessage, message.pluralCount), { // Translation: This comma is used for separating first to penultimate elements in an enumeration. "players": mPlayers.join(translate(", ")), "lastPlayer": lastPlayer }); } function formatDiplomacyMessage(msg) { let messageType; if (g_IsObserver) messageType = "observer"; else if (Engine.GetPlayerID() == msg.sourcePlayer) messageType = "active"; else if (Engine.GetPlayerID() == msg.targetPlayer) messageType = "passive"; else return ""; return sprintf(g_DiplomacyMessages[messageType][msg.status], { "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer), "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer) }); } /** * Optionally show all tributes sent in observer mode and tributes sent between allied players. * Otherwise, only show tributes sent directly to us, and tributes that we send. */ function formatTributeMessage(msg) { let message = ""; if (msg.targetPlayer == Engine.GetPlayerID()) message = translate("%(player)s has sent you %(amounts)s."); else if (msg.sourcePlayer == Engine.GetPlayerID()) message = translate("You have sent %(player2)s %(amounts)s."); else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" && (g_IsObserver || g_GameAttributes.settings.LockTeams && g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] && g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()])) message = translate("%(player)s has sent %(player2)s %(amounts)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.sourcePlayer), "player2": colorizePlayernameByID(msg.targetPlayer), "amounts": getLocalizedResourceAmounts(msg.amounts) }); } function formatBarterMessage(msg) { if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true") return ""; let amountsSold = {}; amountsSold[msg.resourceSold] = msg.amountsSold; let amountsBought = {}; amountsBought[msg.resourceBought] = msg.amountsBought; return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), { "player": colorizePlayernameByID(msg.player), "amountsBought": getLocalizedResourceAmounts(amountsBought), "amountsSold": getLocalizedResourceAmounts(amountsSold) }); } function formatAttackMessage(msg) { if (msg.player != g_ViewedPlayer) return ""; let message = msg.targetIsDomesticAnimal ? translate("Your livestock has been attacked by %(attacker)s!") : translate("You have been attacked by %(attacker)s!"); return sprintf(message, { "attacker": colorizePlayernameByID(msg.attacker) }); } function formatPhaseMessage(msg) { let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase"); if (notifyPhase == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer]) return ""; let message = ""; if (notifyPhase == "all") { if (msg.phaseState == "started") message = translate("%(player)s is advancing to the %(phaseName)s."); else if (msg.phaseState == "aborted") message = translate("The %(phaseName)s of %(player)s has been aborted."); } if (msg.phaseState == "completed") message = translate("%(player)s has reached the %(phaseName)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.player), "phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ)) }); } function formatChatCommand(msg) { if (!msg.text) return ""; let isMe = msg.text.indexOf("/me ") == 0; if (!isMe && !parseChatAddressee(msg)) return ""; isMe = msg.text.indexOf("/me ") == 0; if (isMe) msg.text = msg.text.substr("/me ".length); // Translate or escape text if (!msg.text) return ""; if (msg.translate) { msg.text = translate(msg.text); if (msg.translateParameters) { let parameters = msg.parameters || {}; translateObjectKeys(parameters, msg.translateParameters); msg.text = sprintf(msg.text, parameters); } } else { msg.text = escapeText(msg.text); let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name; if (userName != g_PlayerAssignments[msg.guid].name) notifyUser(userName, msg.text); } // GUID for players, playerID for AIs let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player); return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], { "message": msg.text, "context": msg.context || undefined, "user": coloredUsername, "userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername }) }); } /** * Checks if the current user is an addressee of the chatmessage sent by another player. * Sets the context and potentially addresseeGUID of that message. * Returns true if the message should be displayed. * * @param {Object} msg */ function parseChatAddressee(msg) { if (msg.text[0] != '/') return true; // Split addressee command and message-text msg.cmd = msg.text.split(/\s/)[0]; msg.text = msg.text.substr(msg.cmd.length + 1); // GUID is "local" in singleplayer, some string in multiplayer. // Chat messages sent by the simulation (AI) come with the playerID. let senderID = msg.player ? msg.player : (g_PlayerAssignments[msg.guid] || msg).player; let isSender = msg.guid ? msg.guid == Engine.GetPlayerGUID() : senderID == Engine.GetPlayerID(); // Parse private message let isPM = msg.cmd == "/msg"; let addresseeGUID; let addresseeIndex; if (isPM) { addresseeGUID = matchUsername(msg.text); let addressee = g_PlayerAssignments[addresseeGUID]; if (!addressee) { if (isSender) warn("Couldn't match username: " + msg.text); return false; } // Prohibit PM if addressee and sender are identical if (isSender && addresseeGUID == Engine.GetPlayerGUID()) return false; msg.text = msg.text.substr(addressee.name.length + 1); addresseeIndex = addressee.player; } // Set context string if (!g_ChatAddresseeContext[msg.cmd]) { if (isSender) warn("Unknown chat command: " + msg.cmd); return false; } msg.context = g_ChatAddresseeContext[msg.cmd]; // For observers only permit public- and observer-chat and PM to observers if (isPlayerObserver(senderID) && (isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers")) return false; let visible = isSender || g_IsChatAddressee[msg.cmd](senderID, addresseeGUID); msg.isVisiblePM = isPM && visible; return visible; } /** * Returns the guid of the user with the longest name that is a prefix of the given string. */ function matchUsername(text) { if (!text) return ""; let match = ""; let playerGUID = ""; for (let guid in g_PlayerAssignments) { let pName = g_PlayerAssignments[guid].name; if (text.indexOf(pName + " ") == 0 && pName.length > match.length) { match = pName; playerGUID = guid; } } return playerGUID; } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknow dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } pauseGame(); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 20100) @@ -1,1677 +1,1686 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay)); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); /** * Colors to flash when pop limit reached. */ var g_DefaultPopulationColor = "white"; var g_PopulationAlertColor = "orange"; /** * Seen in the tooltip of the top panel. */ var g_ResourceTitleFont = "sans-bold-16"; /** * A random file will be played. TODO: more variety */ var g_Ambient = ["audio/ambient/dayscape/day_temperate_gen_03.ogg"]; /** * Map, player and match settings set in gamesetup. */ -const g_GameAttributes = Object.freeze(Engine.GetInitAttributes()); +const g_GameAttributes = deepfreeze(Engine.GetInitAttributes()); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController; /** * True if this is a multiplayer game. */ var g_IsNetworked = false; /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * Shows a message box asking the user to leave if "won" or "defeated". */ var g_ConfirmExit = false; /** * True if the current player has paused the game explicitly. */ var g_Paused = false; /** * The list of GUIDs of players who have currently paused the game, if the game is networked. */ var g_PausingClients = []; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; var g_PlayerAssignments = { "local": { "name": singleplayerName(), "player": 1 } }; /** * Cache dev-mode settings that are frequently or widely used. */ var g_DevSettings = { "changePerspective": false, "controlAll": false }; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Blink the population counter if the player can't train more units. */ var g_IsTrainingBlocked = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * Top coordinate of the research list. * Changes depending on the number of displayed counters. */ var g_ResearchListTop = 4; /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Display data of the current players entities shown in the top panel. */ var g_PanelEntities = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "CitizenSoldier"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; /** * Cache the idle worker status. */ var g_HasIdleWorker = false; function GetSimState() { if (!g_SimState) - g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); + g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetEntityState(entId) { if (!g_EntityStates[entId]) + { g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId); + // Freeze all existing properties, but allow GetExtendedEntityState to extend the object + if (g_EntityStates[entId]) + for (let name of Object.getOwnPropertyNames(g_EntityStates[entId])) + if (typeof prop == 'object' && prop !== null) + deepfreeze(g_EntityStates[entId][name]) + } + return g_EntityStates[entId]; } function GetExtendedEntityState(entId) { let entState = GetEntityState(entId); - if (!entState || entState.extended) - return entState; + if (entState && !entState.extended) + { + let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId); + for (let prop in extension) + entState[prop] = extension[prop]; + entState.extended = true; + g_EntityStates[entId] = deepfreeze(entState); + } - let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId); - for (let prop in extension) - entState[prop] = extension[prop]; - entState.extended = true; - g_EntityStates[entId] = entState; - return entState; + return g_EntityStates[entId]; } function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); translateObjectKeys(template, ["specific", "generic", "tooltip"]); - g_TemplateData[templateName] = template; + g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { let template = Engine.GuiInterfaceCall("GetTechnologyData", { "name": technologyName, "civ": civ }); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); - g_TechnologyData[civ][technologyName] = template; + g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } if (initData) { g_IsNetworked = initData.isNetworked; g_IsController = initData.isController; g_PlayerAssignments = initData.playerAssignments; g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked; } else // Needed for autostart loading option { if (g_IsReplay) g_PlayerAssignments.local.player = -1; } updatePlayerData(); g_BarterSell = g_ResourceData.GetCodes()[0]; initializeMusic(); // before changing the perspective let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.list = g_GameSpeeds.Title; gameSpeed.list_data = g_GameSpeeds.Speed; let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate()); gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default; gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }; initMenuPosition(); resizeDiplomacyDialog(); resizeTradeDialog(); for (let slot in Engine.GetGUIObjectByName("panelEntityPanel").children) initPanelEntities(slot); // Populate player selection dropdown let playerNames = [translate("Observer")]; let playerIDs = [-1]; for (let player in g_Players) { playerIDs.push(player); playerNames.push(colorizePlayernameHelper("■", player) + " " + g_Players[player].name); } // Select "observer" item when rejoining as a defeated player let viewedPlayer = g_Players[Engine.GetPlayerID()]; let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer"); viewPlayerDropdown.list = playerNames; viewPlayerDropdown.list_data = playerIDs; viewPlayerDropdown.selected = viewedPlayer && viewedPlayer.state == "defeated" ? 0 : Engine.GetPlayerID() + 1; // If in Atlas editor, disable the exit button if (Engine.IsAtlasRunning()) Engine.GetGUIObjectByName("menuExitButton").enabled = false; if (hotloadData) g_Selection.selected = hotloadData.selection; initChatWindow(); sendLobbyPlayerlistUpdate(); onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); // Report the performance after 5 seconds (when we're still near // the initial camera view) and a minute (when the profiler will // have settled down if framerates as very low), to give some // extremely rough indications of performance // // DISABLED: this information isn't currently useful for anything much, // and it generates a massive amount of data to transmit and store //setTimeout(function() { reportPerformance(5); }, 5000); //setTimeout(function() { reportPerformance(60); }, 60000); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamsLocked": playerState.teamsLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * Depends on the current player (g_IsObserver). */ function updateHotkeyTooltips() { Engine.GetGUIObjectByName("chatInput").tooltip = translateWithContext("chat input", "Type the message to send.") + "\n" + colorizeAutocompleteHotkey() + colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") + colorizeHotkey( "\n" + (g_IsObserver ? translate("Press %(hotkey)s to open the observer chat.") : translate("Press %(hotkey)s to open the ally chat.")), "teamchat") + colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat"); Engine.GetGUIObjectByName("idleWorkerButton").tooltip = colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") + translate("Find idle worker"); Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey( translate("Select one type of goods you want to modify by clicking on it, and then use the arrows of the other types to modify their shares. You can also press %(hotkey)s while selecting one type of goods to bring its share to 100%%."), "session.fulltradeswap"); Engine.GetGUIObjectByName("barterHelp").tooltip = sprintf( translate("Start by selecting the resource you wish to sell from the upper row. For each time the lower buttons are pressed, %(quantity)s of the upper resource will be sold for the displayed quantity of the lower. Press and hold %(hotkey)s to temporarily multiply the traded amount by %(multiplier)s."), { "quantity": g_BarterResourceSellQuantity, "hotkey": colorizeHotkey("%(hotkey)s", "session.massbarter"), "multiplier": g_BarterMultiplier }); } function initPanelEntities(slot) { let button = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]"); button.onPress = function() { let panelEnt = g_PanelEntities.find(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot); if (!panelEnt) return; if (!Engine.HotkeyIsPressed("selection.add")) g_Selection.reset(); g_Selection.addList([panelEnt.ent]); }; button.onDoublePress = function() { let panelEnt = g_PanelEntities.find(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot); if (panelEnt) selectAndMoveTo(getEntityOrHolder(panelEnt.ent)); }; } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && (entState.unitAI.orders[0].type == "Garrison" || entState.unitAI.orders[0].type == "Autogarrison")) return getEntityOrHolder(entState.unitAI.orders[0].data.target); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); playAmbient(); } function toggleChangePerspective(enabled) { g_DevSettings.changePerspective = enabled; selectViewPlayer(g_ViewedPlayer); } /** * Change perspective tool. * Shown to observers or when enabling the developers option. */ function selectViewPlayer(playerID) { if (playerID < -1 || playerID > g_Players.length - 1) return; if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); g_IsObserver = isPlayerObserver(Engine.GetPlayerID()); if (g_IsObserver || g_DevSettings.changePerspective) { if (g_ViewedPlayer != playerID) clearSelection(); g_ViewedPlayer = playerID; } if (g_DevSettings.changePerspective) { Engine.SetPlayerID(g_ViewedPlayer); g_IsObserver = isPlayerObserver(g_ViewedPlayer); } Engine.SetViewedPlayer(g_ViewedPlayer); updateTopPanel(); updateChatAddressees(); updateHotkeyTooltips(); // Update GUI and clear player-dependent cache onSimulationUpdate(); if (g_IsDiplomacyOpen) openDiplomacy(); if (g_IsTradeOpen) openTrade(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. * @param {object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "defeat-victory", "message": victoryString, "players": players }); if (players.indexOf(Engine.GetPlayerID()) != -1) reportGame(); sendLobbyPlayerlistUpdate(); updatePlayerData(); updateChatAddressees(); if (players.indexOf(g_ViewedPlayer) == -1) return; // Select "observer" item on loss. On win enable observermode without changing perspective Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0; if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); g_ConfirmExit = won ? "won" : "defeated"; } /** * Sets civ icon for the currently viewed player. * Hides most gui objects for observers. */ function updateTopPanel() { let isPlayer = g_ViewedPlayer > 0; let civIcon = Engine.GetGUIObjectByName("civIcon"); civIcon.hidden = !isPlayer; if (isPlayer) { civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem; Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), { "civ": g_CivData[g_Players[g_ViewedPlayer].civ].Name }); } Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || !isPlayer; let viewPlayer = Engine.GetGUIObjectByName("viewPlayer"); viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective; let followPlayerLabel = Engine.GetGUIObjectByName("followPlayerLabel"); followPlayerLabel.hidden = Engine.GetTextWidth(followPlayerLabel.font, followPlayerLabel.caption + " ") + followPlayerLabel.getComputedSize().left > viewPlayer.getComputedSize().left; let resCodes = g_ResourceData.GetCodes(); let r = 0; for (let res of resCodes) { if (!Engine.GetGUIObjectByName("resource["+r+"]")) { warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!"); break; } Engine.GetGUIObjectByName("resource["+r+"]_icon").sprite = "stretched:session/icons/resources/" + res + ".png"; Engine.GetGUIObjectByName("resource["+r+"]").hidden = !isPlayer; ++r; } horizontallySpaceObjects("resourceCounts", 5); hideRemaining("resourceCounts", r); let resPop = Engine.GetGUIObjectByName("population"); let resPopSize = resPop.size; resPopSize.left = Engine.GetGUIObjectByName("resource["+ (r-1) +"]").size.right; resPop.size = resPopSize; Engine.GetGUIObjectByName("population").hidden = !isPlayer; Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("observerText").hidden = isPlayer; let alphaLabel = Engine.GetGUIObjectByName("alphaLabel"); alphaLabel.hidden = isPlayer && !viewPlayer.hidden; alphaLabel.size = isPlayer ? "50%+54 0 100%-293 100%" : "155 0 85%-279 100%"; Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked || g_IsController; Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver; } function reportPerformance(time) { let settings = g_GameAttributes.settings; Engine.SubmitUserReport("profile", 3, JSON.stringify({ "time": time, "map": settings.Name, "seed": settings.Seed, // only defined for random maps "size": settings.Size, // only defined for random maps "profiler": Engine.GetProfilerState() })); } /** * Resign a player. * @param leaveGameAfterResign If player is quitting after resignation. */ function resignGame(leaveGameAfterResign) { if (g_IsObserver || g_Disconnected) return; Engine.PostNetworkCommand({ "type": "resign" }); if (!leaveGameAfterResign) resumeGame(true); } /** * Leave the game * @param willRejoin If player is going to be rejoining a networked game. */ function leaveGame(willRejoin) { if (!willRejoin && !g_IsObserver) resignGame(true); // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = getReplayMetadata(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "assignedPlayer": Engine.GetPlayerID(), "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; updateGUIObjects(); // Display rally points for selected buildings if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); updateMenuPosition(tickLength); // When training is blocked, flash population (alternates color every 500msec) Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && now % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor; Engine.GuiInterfaceCall("ClearRenamedEntities"); } function onWindowResized() { // Update followPlayerLabel updateTopPanel(); resizeChatWindow(); } function changeGameSpeed(speed) { if (!g_IsNetworked) Engine.SetSimRate(speed); } function hasIdleWorker() { return Engine.GuiInterfaceCall("HasIdleUnits", { "viewedPlayer": g_ViewedPlayer, "idleClasses": g_WorkerTypes, "excludeUnits": [] }); } function updateIdleWorkerButton() { g_HasIdleWorker = hasIdleWorker(); let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay"); let prefix = "stretched:session/"; if (!g_HasIdleWorker) idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png"; else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png") idleWorkerButton.sprite = prefix + "minimap-idle.png"; } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded every turn. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; g_TemplateData = {}; + g_SimState = undefined; - g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); - if (!g_SimState) + if (!GetSimState()) return; updateCinemaPath(); handleNotifications(); updateGUIObjects(); for (let type of ["Aura", "Heal"]) Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", { "type": type, "enabled": Engine.ConfigDB_GetValue("user", "gui.session." + type.toLowerCase() + "range") == "true" }); if (g_ConfirmExit) confirmExit(); } /** * Don't show the message box before all playerstate changes are processed. */ function confirmExit() { if (g_IsNetworked && !g_IsNetworkedActive) return; closeOpenDialogs(); // Don't ask for exit if other humans are still playing let isHost = g_IsController && g_IsNetworked; let askExit = !isHost || isHost && g_Players.every((player, i) => i == 0 || player.state != "active" || g_GameAttributes.settings.PlayerData[i].AI != ""); let subject = g_PlayerStateMessages[g_ConfirmExit]; if (askExit) subject += "\n" + translate("Do you want to quit?"); messageBox( 400, 200, subject, g_ConfirmExit == "won" ? translate("VICTORIOUS!") : translate("DEFEATED!"), askExit ? [translate("No"), translate("Yes")] : [translate("OK")], askExit ? [resumeGame, leaveGame] : [resumeGame] ); g_ConfirmExit = false; } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("sn").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true"); } function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updatePanelEntities(); displayPanelEntities(); updateGroups(); updateDebug(); updatePlayerDisplay(); updateResearchDisplay(); updateSelectionDetails(); updateBuildingPlacementPreview(); updateTimeNotifications(); updateIdleWorkerButton(); if (g_IsTradeOpen) { updateTraderTexts(); updateBarterButtons(); } if (g_ViewedPlayer > 0) { let playerState = GetSimState().players[g_ViewedPlayer]; g_DevSettings.controlAll = playerState && playerState.controlsAll; Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll; } if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } updateDiplomacy(); } function onReplayFinished() { closeOpenDialogs(); pauseGame(); messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished", "Confirmation"), [translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")], [resumeGame, leaveGame]); } /** * updates a status bar on the GUI * nameOfBar: name of the bar * points: points to show * maxPoints: max points * direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3; */ function updateGUIStatusBar(nameOfBar, points, maxPoints, direction) { // check, if optional direction parameter is valid. if (!direction || !(direction >= 0 && direction < 4)) direction = 0; // get the bar and update it let statusBar = Engine.GetGUIObjectByName(nameOfBar); if (!statusBar) return; let healthSize = statusBar.size; let value = 100*Math.max(0, Math.min(1, points / maxPoints)); // inverse bar if (direction == 2 || direction == 3) value = 100 - value; if (direction == 0) healthSize.rright = value; else if (direction == 1) healthSize.rbottom = value; else if (direction == 2) healthSize.rleft = value; else if (direction == 3) healthSize.rtop = value; statusBar.size = healthSize; } function updatePanelEntities() { let panelEnts = g_ViewedPlayer == -1 ? GetSimState().players.reduce((ents, pState) => ents.concat(pState.panelEntities), []) : GetSimState().players[g_ViewedPlayer].panelEntities; g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent)); for (let ent of panelEnts) { let panelEntState = GetExtendedEntityState(ent); let template = GetTemplateData(panelEntState.template); let panelEnt = g_PanelEntities.find(panelEnt => ent == panelEnt.ent); if (!panelEnt) { panelEnt = { "ent": ent, "tooltip": undefined, "sprite": "stretched:session/portraits/" + template.icon, "maxHitpoints": undefined, "currentHitpoints": panelEntState.hitpoints, "previousHitpoints": undefined }; g_PanelEntities.push(panelEnt); } panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template); panelEnt.previousHitpoints = panelEnt.currentHitpoints; panelEnt.currentHitpoints = panelEntState.hitpoints; panelEnt.maxHitpoints = panelEntState.maxHitpoints; } let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass => GetEntityState(ent).identity.classes.indexOf(entClass) != -1); g_PanelEntities = g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent)); } function createPanelEntityTooltip(panelEntState, template) { let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]"; return [ getPanelEntNameTooltip, getCurrentHealthTooltip, getAttackTooltip, getArmorTooltip, getEntityTooltip, getAurasTooltip ].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n"); } function displayPanelEntities() { let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children; buttons.forEach((button, slot) => { if (button.hidden || g_PanelEntities.some(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot)) return; button.hidden = true; stopColorFade("panelEntityHitOverlay[" + slot + "]"); }); // The slot identifies the button, displayIndex determines its position. for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex) { let panelEnt = g_PanelEntities[displayIndex]; // Find the first unused slot if new, otherwise reuse previous. let slot = panelEnt.slot === undefined ? buttons.findIndex(button => button.hidden) : panelEnt.slot; let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]"); panelEntButton.tooltip = panelEnt.tooltip; updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints); if (panelEnt.slot === undefined) { let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]"); panelEntImage.sprite = panelEnt.sprite; panelEntButton.hidden = false; panelEnt.slot = slot; } // If the health of the panelEnt changed since the last update, trigger the animation. if (panelEnt.previousHitpoints > panelEnt.currentHitpoints) startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit); // TODO: Instead of instant position changes, animate button movement. setPanelObjectPosition(panelEntButton, displayIndex, buttons.length); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i; let button = Engine.GetGUIObjectByName("unitGroupButton["+i+"]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Chose icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } function updateDebug() { let debug = Engine.GetGUIObjectByName("debugEntityState"); if (!Engine.GetGUIObjectByName("devDisplayState").checked) { debug.hidden = true; return; } debug.hidden = false; let conciseSimState = deepcopy(GetSimState()); conciseSimState.players = "<<>>"; let text = "simulation: " + uneval(conciseSimState); let selection = g_Selection.toList(); if (selection.length) { let entState = GetExtendedEntityState(selection[0]); if (entState) { let template = GetTemplateData(entState.template); text += "\n\nentity: {\n"; for (let k in entState) text += " "+k+":"+uneval(entState[k])+"\n"; text += "}\n\ntemplate: " + uneval(template); } } debug.caption = text.replace(/\[/g, "\\["); } function getAllyStatTooltip(resource) { let playersState = GetSimState().players; let ret = ""; for (let player in playersState) { if (player != 0 && player != g_ViewedPlayer && g_Players[player].state != "defeated" && (g_IsObserver || playersState[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer])) { ret += "\n" + sprintf(translate("%(playername)s: %(statValue)s"),{ "playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name, "statValue": resource == "pop" ? sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playersState[player]) : Math.round(playersState[player].resourceCounts[resource]) }); } } return ret; } function updatePlayerDisplay() { let playerState = GetSimState().players[g_ViewedPlayer]; if (!playerState) return; let resCodes = g_ResourceData.GetCodes(); for (let r = 0; r < resCodes.length; ++r) { let resourceObj = Engine.GetGUIObjectByName("resource[" + r + "]"); if (!resourceObj) break; let res = resCodes[r]; let tooltip = '[font="' + g_ResourceTitleFont + '"]' + resourceNameFirstWord(res) + '[/font]'; let descr = g_ResourceData.GetResource(res).description; if (descr) tooltip += "\n" + translate(descr); tooltip += getAllyStatTooltip(res); resourceObj.tooltip = tooltip; Engine.GetGUIObjectByName("resource["+r+"]_count").caption = Math.floor(playerState.resourceCounts[res]); } Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState); Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" + sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }) + getAllyStatTooltip("pop"); g_IsTrainingBlocked = playerState.trainingBlocked; } function selectAndMoveTo(ent) { let entState = GetEntityState(ent); if (!entState || !entState.position) return; g_Selection.reset(); g_Selection.addList([ent]); let position = entState.position; Engine.CameraMoveTo(position.x, position.z); } function updateResearchDisplay() { let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer); // Set up initial positioning. let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right; for (let i = 0; i < 10; ++i) { let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]"); let size = button.size; size.top = g_ResearchListTop + (4 + buttonSideLength) * i; size.bottom = size.top + buttonSideLength; button.size = size; } let numButtons = 0; for (let tech in researchStarted) { // Show at most 10 in-progress techs. if (numButtons >= 10) break; let template = GetTechnologyData(tech, g_Players[g_ViewedPlayer].civ); let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]"); button.hidden = false; button.tooltip = getEntityNames(template); button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher); let icon = "stretched:session/portraits/" + template.icon; Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon; // Scale the progress indicator. let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left)); Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size; ++numButtons; } // Hide unused buttons. for (let i = numButtons; i < 10; ++i) Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true; } /** * Toggles the display of status bars for all of the player's entities. * * @param {Boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove }); } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateValue("user", configName, String(enabled)); Engine.ConfigDB_WriteValueToFile("user", configName, String(enabled), "config/user.cfg"); return enabled; } /** * Toggles the display of range overlays of selected entities for the given range type. * @param {string} type - for example "Aura" */ function toggleRangeOverlay(type) { let enabled = toggleConfigBool("gui.session." + type.toLowerCase() + "range"); Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", { "type": type, "enabled": enabled }); let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); Engine.GuiInterfaceCall("SetRangeOverlays", { "entities": selected, "enabled": enabled }); } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) { // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } } if (g_ShowGuarded) { // flag the guarded entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } function playAmbient() { Engine.PlayAmbientSound(pickRandom(g_Ambient), true); } function getBuildString() { return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { "buildDate": Engine.GetBuildTimestamp(0), "revision": Engine.GetBuildTimestamp(2) }); } function showTimeWarpMessageBox() { messageBox( 500, 250, translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."), translate("Time warp mode") ); } /** * Adds the ingame time and ceasefire counter to the global FPS and * realtime counters shown in the top right corner. */ function appendSessionCounters(counters) { let simState = GetSimState(); if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true") { let currentSpeed = Engine.GetSimRate(); if (currentSpeed != 1.0) // Translation: The "x" means "times", with the mathematical meaning of multiplication. counters.push(sprintf(translate("%(time)s (%(speed)sx)"), { "time": timeToString(simState.timeElapsed), "speed": Engine.FormatDecimalNumberIntoString(currentSpeed) })); else counters.push(timeToString(simState.timeElapsed)); } if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true") counters.push(timeToString(simState.ceasefireTimeRemaining)); g_ResearchListTop = 4 + 14 * counters.length; } /** * Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby. * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. */ function sendLobbyPlayerlistUpdate() { if (!g_IsController || !Engine.HasXmppClient()) return; // Extract the relevant player data and minimize packet load let minPlayerData = []; for (let playerID in g_GameAttributes.settings.PlayerData) { if (+playerID == 0) continue; let pData = g_GameAttributes.settings.PlayerData[playerID]; let minPData = { "Name": pData.Name }; if (g_GameAttributes.settings.LockTeams) minPData.Team = pData.Team; if (pData.AI) { minPData.AI = pData.AI; minPData.AIDiff = pData.AIDiff; } if (g_Players[playerID].offline) minPData.Offline = true; // Whether the player has won or was defeated let state = g_Players[playerID].state; if (state != "active") minPData.State = state; minPlayerData.push(minPData); } // Add observers let connectedPlayers = 0; for (let guid in g_PlayerAssignments) { let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player]; if (pData) ++connectedPlayers; else minPlayerData.push({ "Name": g_PlayerAssignments[guid].name, "Team": "observer" }); } Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData)); } /** * Send a report on the gamestatus to the lobby. */ function reportGame() { // Only 1v1 games are rated (and Gaia is part of g_Players) if (!Engine.HasXmppClient() || !Engine.IsRankedGame() || g_Players.length != 3 || Engine.GetPlayerID() == -1) return; let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); let unitsClasses = [ "total", "Infantry", "Worker", "FemaleCitizen", "Cavalry", "Champion", "Hero", "Siege", "Ship", "Trader" ]; let unitsCountersTypes = [ "unitsTrained", "unitsLost", "enemyUnitsKilled" ]; let buildingsClasses = [ "total", "CivCentre", "House", "Economic", "Outpost", "Military", "Fortress", "Wonder" ]; let buildingsCountersTypes = [ "buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed" ]; let resourcesTypes = [ "wood", "food", "stone", "metal" ]; let resourcesCounterTypes = [ "resourcesGathered", "resourcesUsed", "resourcesSold", "resourcesBought" ]; let misc = [ "tradeIncome", "tributesSent", "tributesReceived", "treasuresCollected", "lootCollected", "percentMapExplored" ]; let playerStatistics = {}; // Unit Stats for (let unitCounterType of unitsCountersTypes) { if (!playerStatistics[unitCounterType]) playerStatistics[unitCounterType] = { }; for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] = ""; } playerStatistics.unitsLostValue = ""; playerStatistics.unitsKilledValue = ""; // Building stats for (let buildingCounterType of buildingsCountersTypes) { if (!playerStatistics[buildingCounterType]) playerStatistics[buildingCounterType] = { }; for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] = ""; } playerStatistics.buildingsLostValue = ""; playerStatistics.enemyBuildingsDestroyedValue = ""; // Resources for (let resourcesCounterType of resourcesCounterTypes) { if (!playerStatistics[resourcesCounterType]) playerStatistics[resourcesCounterType] = { }; for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] = ""; } playerStatistics.resourcesGathered.vegetarianFood = ""; for (let type of misc) playerStatistics[type] = ""; // Total playerStatistics.economyScore = ""; playerStatistics.militaryScore = ""; playerStatistics.totalScore = ""; let mapName = g_GameAttributes.settings.Name; let playerStates = ""; let playerCivs = ""; let teams = ""; let teamsLocked = true; // Serialize the statistics for each player into a comma-separated list. // Ignore gaia for (let i = 1; i < extendedSimState.players.length; ++i) { let player = extendedSimState.players[i]; let maxIndex = player.sequences.time.length - 1; playerStates += player.state + ","; playerCivs += player.civ + ","; teams += player.team + ","; teamsLocked = teamsLocked && player.teamsLocked; for (let resourcesCounterType of resourcesCounterTypes) for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ","; playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ","; for (let unitCounterType of unitsCountersTypes) for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ","; for (let buildingCounterType of buildingsCountersTypes) for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ","; let total = 0; for (let type in player.sequences.resourcesGathered) total += player.sequences.resourcesGathered[type][maxIndex]; playerStatistics.economyScore += total + ","; playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ","; playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ","; for (let type of misc) playerStatistics[type] += player.sequences[type][maxIndex] + ","; } // Send the report with serialized data let reportObject = {}; reportObject.timeElapsed = extendedSimState.timeElapsed; reportObject.playerStates = playerStates; reportObject.playerID = Engine.GetPlayerID(); reportObject.matchID = g_GameAttributes.matchID; reportObject.civs = playerCivs; reportObject.teams = teams; reportObject.teamsLocked = String(teamsLocked); reportObject.ceasefireActive = String(extendedSimState.ceasefireActive); reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining); reportObject.mapName = mapName; reportObject.economyScore = playerStatistics.economyScore; reportObject.militaryScore = playerStatistics.militaryScore; reportObject.totalScore = playerStatistics.totalScore; for (let rct of resourcesCounterTypes) { for (let rt of resourcesTypes) reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt]; // eg. rt = food rct.substr = Gathered rct = resourcesGathered } reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood; for (let type of unitsClasses) { // eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type]; reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type]; } for (let type of buildingsClasses) { reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type]; reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type]; } for (let type of misc) reportObject[type] = playerStatistics[type]; Engine.SendGameReport(reportObject); } Index: ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (revision 20099) +++ ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (revision 20100) @@ -1,60 +1,63 @@ /** * System component which loads the technology and the aura data files */ function DataTemplateManager() {} DataTemplateManager.prototype.Schema = ""; DataTemplateManager.prototype.Init = function() { this.allTechs = {}; this.allAuras = {}; for (let techName of this.ListAllTechs()) this.GetTechnologyTemplate(techName); for (let auraName of this.ListAllAuras()) this.GetAuraTemplate(auraName); + + deepfreeze(this.allTechs); + deepfreeze(this.allAuras); }; DataTemplateManager.prototype.GetTechnologyTemplate = function(template) { if (!this.allTechs[template]) { this.allTechs[template] = Engine.ReadJSONFile("technologies/" + template + ".json"); if (!this.allTechs[template]) error("Failed to load technology \"" + template + "\""); } return this.allTechs[template]; }; DataTemplateManager.prototype.GetAuraTemplate = function(template) { if (!this.allAuras[template]) { this.allAuras[template] = Engine.ReadJSONFile("auras/" + template + ".json"); if (!this.allAuras[template]) error("Failed to load aura \"" + template + "\""); } return this.allAuras[template]; }; DataTemplateManager.prototype.ListAllTechs = function() { return Engine.FindJSONFiles("technologies", true); }; DataTemplateManager.prototype.ListAllAuras = function() { return Engine.FindJSONFiles("auras", true); }; DataTemplateManager.prototype.GetAllTechs = function() { return this.allTechs; }; Engine.RegisterSystemComponentType(IID_DataTemplateManager, "DataTemplateManager", DataTemplateManager); Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp (revision 20099) +++ ps/trunk/source/ps/SavedGame.cpp (revision 20100) @@ -1,310 +1,312 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "SavedGame.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/archive/archive_zip.h" #include "i18n/L10n.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" static const int SAVED_GAME_VERSION_MAJOR = 1; // increment on incompatible changes to the format static const int SAVED_GAME_VERSION_MINOR = 0; // increment on compatible changes to the format // TODO: we ought to check version numbers when loading files Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d"); const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave"); VfsPath filename; // Don't make this a static global like NextNumberedFilename expects, because // that wouldn't work when 'prefix' changes, and because it's not thread-safe size_t nextSaveNumber = 0; vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename); return Save(filename.Filename().string(), description, simulation, guiMetadataClone); } Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { JSContext* cx = simulation.GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + name); const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave"); // ArchiveWriter_Zip can only write to OsPaths, not VfsPaths, // but we'd like to handle saved games via VFS. // To avoid potential confusion from writing with non-VFS then // reading the same file with VFS, we'll just write to a temporary // non-VFS path and then load and save again via VFS, // which is kind of a hack. OsPath tempSaveFileRealPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath)); tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave"; time_t now = time(NULL); // Construct the serialized state to be saved std::stringstream simStateStream; if (!simulation.SerializeState(simStateStream)) WARN_RETURN(ERR::FAIL); JS::RootedValue metadata(cx); JS::RootedValue initAttributes(cx, simulation.GetInitAttributes()); simulation.GetScriptInterface().Eval("({})", &metadata); simulation.GetScriptInterface().SetProperty(metadata, "version_major", SAVED_GAME_VERSION_MAJOR); simulation.GetScriptInterface().SetProperty(metadata, "version_minor", SAVED_GAME_VERSION_MINOR); simulation.GetScriptInterface().SetProperty(metadata, "engine_version", std::string(engine_version)); simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded); simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now); simulation.GetScriptInterface().SetProperty(metadata, "playerID", g_Game->GetPlayerID()); simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes); JS::RootedValue guiMetadata(cx); simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata); // get some camera data JS::RootedValue cameraMetadata(cx); simulation.GetScriptInterface().Eval("({})", &cameraMetadata); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosX", g_Game->GetView()->GetCameraPosX()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosY", g_Game->GetView()->GetCameraPosY()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosZ", g_Game->GetView()->GetCameraPosZ()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotX", g_Game->GetView()->GetCameraRotX()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotY", g_Game->GetView()->GetCameraRotY()); simulation.GetScriptInterface().SetProperty(cameraMetadata, "Zoom", g_Game->GetView()->GetCameraZoom()); simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata); simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata); simulation.GetScriptInterface().SetProperty(metadata, "description", description); std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true); // Write the saved game as zip file containing the various components PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false); if (!archiveWriter) WARN_RETURN(ERR::FAIL); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json")); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat")); archiveWriter.reset(); // close the file WriteBuffer buffer; CFileInfo tempSaveFile; WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile)); buffer.Reserve(tempSaveFile.Size()); WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size())); WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size())); OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8()); debug_printf("Saved game to '%s'\n", realPath.string8().c_str()); return INFO::OK; } /** * Helper class for retrieving data from saved game archives */ class CGameLoader { NONCOPYABLE(CGameLoader); public: /** * @param scriptInterface the ScriptInterface used for loading metadata. * @param[out] savedState serialized simulation state stored as string of bytes, * loaded from simulation.dat inside the archive. * * Note: We use a different approach for returning the string and the metadata JS::Value. * We use a pointer for the string to avoid copies (efficiency). We don't use this approach * for the metadata because it would be error prone with rooting and the stack-based rooting * types and confusing (a chain of pointers pointing to other pointers). */ CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) : m_ScriptInterface(scriptInterface), m_Metadata(scriptInterface.GetJSRuntime()), m_SavedState(savedState) { } static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile); } void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); if (pathname == L"metadata.json") { std::string buffer; buffer.resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size())); m_ScriptInterface.ParseJSON(buffer, &m_Metadata); } else if (pathname == L"simulation.dat" && m_SavedState) { m_SavedState->resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size())); } } JS::Value GetMetadata() { return m_Metadata.get(); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Metadata; std::string* m_SavedState; }; Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) { // Determine the filename to load const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); // Don't crash just because file isn't found, this can happen if the file is deleted from the OS if (!VfsFileExists(filename)) return ERR::FILE_NOT_FOUND; OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) WARN_RETURN(ERR::FAIL); CGameLoader loader(scriptInterface, &savedState); WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); metadata.set(loader.GetMetadata()); return INFO::OK; } JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface) { TIMER(L"GetSavedGames"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject games(cx, JS_NewArrayObject(cx, 0)); Status err; VfsPaths pathnames; err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames); WARN_IF_ERR(err); for (size_t i = 0; i < pathnames.size(); ++i) { OsPath realPath; err = g_VFS->GetRealPath(pathnames[i], realPath); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) { // Triggered by e.g. the file being open in another program LOGWARNING("Failed to read saved game '%s'", realPath.string8()); continue; // skip this file } CGameLoader loader(scriptInterface, NULL); err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } JS::RootedValue metadata(cx, loader.GetMetadata()); JS::RootedValue game(cx); scriptInterface.Eval("({})", &game); scriptInterface.SetProperty(game, "id", pathnames[i].Basename()); scriptInterface.SetProperty(game, "metadata", metadata); JS_SetElement(cx, games, i, game); } return JS::ObjectValue(*games); } bool SavedGames::DeleteSavedGame(const std::wstring& name) { const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); OsPath realpath; // Make sure it exists in VFS and find its real path if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK) return false; // Error // Remove from VFS if (g_VFS->RemoveFile(filename) != INFO::OK) return false; // Error // Delete actual file if (wunlink(realpath) != 0) return false; // Error // Successfully deleted file return true; } JS::Value SavedGames::GetEngineInfo(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue metainfo(cx); scriptInterface.Eval("({})", &metainfo); scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR); scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR); scriptInterface.SetProperty(metainfo, "engine_version", std::string(engine_version)); scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded); + + scriptInterface.FreezeObject(metainfo, true); + return metainfo; } -