Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 22520) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 22521) @@ -1,2771 +1,2771 @@ 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_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_TriggerDifficulties = prepareForDropdown(g_Settings && g_Settings.TriggerDifficulties); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_GameSpeeds = getGameSpeedChoices(false); /** * Offer users to select playable civs only. * Load unselectable civs as they could appear in scenario maps. */ const g_CivData = loadCivData(false, false); /** * Store civilization code and page (structree or history) opened in civilization info. */ var g_CivInfo = { "code": "", "page": "page_civinfo.xml" }; /** * 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; /** * Array of biome identifiers supported by the currently selected map. */ var g_BiomeList; /** * Array of trigger difficulties identifiers supported by the currently selected map. */ var g_TriggerDifficultyList; /** * Whether this is a single- or multiplayer match. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ const g_IsController = !g_IsNetworked || Engine.HasNetServer(); /** * 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; /** * 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 or doubleclicking on the "Start Game" button. */ 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 = []; /** * Minimum amount of pixels required for the chat panel to be visible. */ var g_MinChatWidth = 96; /** * Horizontal space between chat window and settings. */ var g_ChatSettingsMargin = 10; /** * 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; /** * Horizontal space between tab buttons and lobby button. */ var g_LobbyButtonSpacing = 8; /** * Vertical size of a tab button. */ var g_TabButtonHeight = 30; /** * Vertical space between two tab buttons. */ var g_TabButtonDist = 4; /** * Vertical size of a setting object. */ var g_SettingHeight = 32; /** * Vertical space between two setting objects. */ var g_SettingDist = 2; /** * Maximum width of a column in the settings panel. */ var g_MaxColumnWidth = 470; /** * Pixels per millisecond the settings panel slides when opening/closing. */ var g_SlideSpeed = 1.2; /** * Store last tick time. */ var g_LastTickTime = Date.now(); /** * Order in which the GUI elements will be shown. * All valid settings are required to appear here. */ var g_SettingsTabsGUI = [ { "label": translateWithContext("Match settings tab name", "Map"), "settings": [ "mapType", "mapFilter", "mapSelection", "mapSize", "biome", "triggerDifficulty", "nomad", "disableTreasures", "exploreMap", "revealMap" ] }, { "label": translateWithContext("Match settings tab name", "Player"), "settings": [ "numPlayers", "populationCap", "startingResources", "disableSpies", "enableCheats" ] }, { "label": translateWithContext("Match settings tab name", "Game Type"), "settings": [ ...g_VictoryConditions.map(victoryCondition => victoryCondition.Name), "relicCount", "relicDuration", "wonderDuration", "regicideGarrison", "gameSpeed", "ceasefire", "lockTeams", "lastManStanding", "enableRating" ] } ]; /** * 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. * initOrder - Settings with lower values will be initialized first. */ 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": (itemIdx) => { g_MapData = {}; g_GameAttributes.mapType = g_MapTypes.Name[itemIdx]; g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType]; delete g_GameAttributes.map; if (g_GameAttributes.mapType != "scenario") g_GameAttributes.settings = { "PlayerData": clone(g_DefaultPlayerData.slice(0, 4)) }; reloadMapFilterList(); }, "autocomplete": 0, "initOrder": 1 }, "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": (itemIdx) => { g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx]; delete g_GameAttributes.map; reloadMapList(); }, "autocomplete": 0, "initOrder": 2 }, "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": (itemIdx) => { selectMap(g_MapSelectionList.file[itemIdx]); }, "autocomplete": 0, "initOrder": 3 }, "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": (itemIdx) => { g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx]; }, "hidden": () => g_GameAttributes.mapType != "random", "autocomplete": 0, "initOrder": 1000 }, "biome": { "title": () => translate("Biome"), "tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."), "labels": () => g_BiomeList ? g_BiomeList.Title : [], "colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [], "ids": () => g_BiomeList ? g_BiomeList.Id : [], "default": () => 0, "defined": () => g_GameAttributes.settings.Biome !== undefined, "get": () => g_GameAttributes.settings.Biome, "select": (itemIdx) => { g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx]; }, "hidden": () => !g_BiomeList, "autocomplete": 0, "initOrder": 1000 }, "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": (itemIdx) => { let num = itemIdx + 1; let pData = g_GameAttributes.settings.PlayerData; g_GameAttributes.settings.PlayerData = num > pData.length ? pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) : pData.slice(0, num); unassignInvalidPlayers(num); sanitizePlayerData(g_GameAttributes.settings.PlayerData); }, "initOrder": 1000 }, "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 coloredText( sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), { "players": players, "popCap": popCap }), "orange"); }, "labels": () => g_PopulationCapacities.Title, "ids": () => g_PopulationCapacities.Population, "default": () => g_PopulationCapacities.Default, "defined": () => g_GameAttributes.settings.PopulationCap !== undefined, "get": () => g_GameAttributes.settings.PopulationCap, "select": (itemIdx) => { g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "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": (itemIdx) => { g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx]; }, "hidden": () => g_GameAttributes.mapType == "scenario", "autocomplete": 0, "initOrder": 1000 }, "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": (itemIdx) => { g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "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": (itemIdx) => { g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx]; }, "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "relicDuration": { "title": () => translate("Relic Duration"), "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."), "labels": () => g_VictoryDurations.Title, "ids": () => g_VictoryDurations.Duration, "default": () => g_VictoryDurations.Default, "defined": () => g_GameAttributes.settings.RelicDuration !== undefined, "get": () => g_GameAttributes.settings.RelicDuration, "select": (itemIdx) => { g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx]; }, "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "wonderDuration": { "title": () => translate("Wonder Duration"), "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."), "labels": () => g_VictoryDurations.Title, "ids": () => g_VictoryDurations.Duration, "default": () => g_VictoryDurations.Default, "defined": () => g_GameAttributes.settings.WonderDuration !== undefined, "get": () => g_GameAttributes.settings.WonderDuration, "select": (itemIdx) => { g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx]; }, "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("wonder") == -1, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "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 && g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1, "get": () => g_GameAttributes.gameSpeed, "select": (itemIdx) => { g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx]; }, "initOrder": 1000 }, "triggerDifficulty": { "title": () => translate("Difficulty"), "tooltip": (hoverIdx) => g_TriggerDifficultyList && g_TriggerDifficultyList.Description[hoverIdx] || translate("Select the difficulty of this scenario."), "labels": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Title : [], "ids": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Id : [], "default": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Default : 0, "defined": () => g_GameAttributes.settings.TriggerDifficulty !== undefined, "get": () => g_GameAttributes.settings.TriggerDifficulty, "select": (itemIdx) => { g_GameAttributes.settings.TriggerDifficulty = g_TriggerDifficultyList && g_TriggerDifficultyList.Id[itemIdx]; }, "hidden": () => !g_TriggerDifficultyList, "initOrder": 1000 }, }; /** * These dropdowns provide a setting that is repeated once for each player * (where playerIdx is starting from 0 for player 1). */ var g_PlayerDropdowns = { "playerAssignment": { "tooltip": (playerIdx) => translate("Select player."), "labels": (playerIdx) => g_PlayerAssignmentList.Name || [], "colors": (playerIdx) => g_PlayerAssignmentList.Color || [], "ids": (playerIdx) => g_PlayerAssignmentList.Choice || [], "default": (playerIdx) => "ai:petra", "defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length, "get": (playerIdx) => { for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player == playerIdx + 1) return "guid:" + guid; for (let ai of g_Settings.AIDescriptions) if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id) return "ai:" + ai.id; return "unassigned"; }, "select": (selectedIdx, playerIdx) => { let choice = g_PlayerAssignmentList.Choice[selectedIdx]; if (choice == "unassigned" || choice.startsWith("ai:")) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerIdx+1, ""); else if (g_PlayerAssignments.local.player == playerIdx+1) g_PlayerAssignments.local.player = -1; g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : ""; } else swapPlayers(choice.substr("guid:".length), playerIdx); }, "autocomplete": 100, }, "playerTeam": { "tooltip": (playerIdx) => translate("Select player's team."), "labels": (playerIdx) => g_PlayerTeamList.label, "ids": (playerIdx) => g_PlayerTeamList.id, "default": (playerIdx) => 0, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined, "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team, "select": (selectedIdx, playerIdx) => { g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "playerCiv": { "tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Choose the civilization for this player."), "labels": (playerIdx) => g_PlayerCivList.name, "colors": (playerIdx) => g_PlayerCivList.color, "ids": (playerIdx) => g_PlayerCivList.code, "default": (playerIdx) => 0, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined, "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ, "select": (selectedIdx, playerIdx) => { g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "autocomplete": 90, }, "playerColorPicker": { "tooltip": (playerIdx) => translate("Pick a color."), "labels": (playerIdx) => g_PlayerColorPickerList.map(color => "■"), "colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)), "ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index), "default": (playerIdx) => playerIdx, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined, "get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color), "select": (selectedIdx, playerIdx) => { 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[playerIdx].Color; playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx]; ensureUniquePlayerColors(playerData); }, "enabled": () => g_GameAttributes.mapType != "scenario", }, }; /** * Contains the logic of all boolean gamesettings. */ var g_Checkboxes = Object.assign( {}, g_VictoryConditions.reduce((obj, victoryCondition) => { obj[victoryCondition.Name] = { "title": () => victoryCondition.Title, "tooltip": () => victoryCondition.Description, // Defaults are set in supplementDefault directly from g_VictoryConditions since we use an array "defined": () => true, "get": () => g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) != -1, "set": checked => { if (checked) { g_GameAttributes.settings.VictoryConditions.push(victoryCondition.Name); if (victoryCondition.ChangeOnChecked) for (let setting in victoryCondition.ChangeOnChecked) g_Checkboxes[setting].set(victoryCondition.ChangeOnChecked[setting]); } else g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions.filter(victoryConditionName => victoryConditionName != victoryCondition.Name); }, "enabled": () => g_GameAttributes.mapType != "scenario" && (!victoryCondition.DisabledWhenChecked || victoryCondition.DisabledWhenChecked.every(victoryConditionName => g_GameAttributes.settings.VictoryConditions.indexOf(victoryConditionName) == -1)) }; return obj; }, {}), { "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.VictoryConditions.indexOf("regicide") == -1, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "nomad": { "title": () => translate("Nomad"), "tooltip": () => translate("In Nomad mode, players start with only few units and have to find a suitable place to build their city. Ceasefire is recommended."), "default": () => false, "defined": () => g_GameAttributes.settings.Nomad !== undefined, "get": () => g_GameAttributes.settings.Nomad, "set": checked => { g_GameAttributes.settings.Nomad = checked; }, "hidden": () => g_GameAttributes.mapType != "random", "initOrder": 1000 }, "revealMap": { "title": () => // Translation: Make sure to differentiate between the revealed map and explored map settings! translate("Revealed Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map settings! () => 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", "initOrder": 1000 }, "exploreMap": { "title": // Translation: Make sure to differentiate between the revealed map and explored map settings! () => translate("Explored Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map settings! () => 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, "initOrder": 1000 }, "disableTreasures": { "title": () => translate("Disable Treasures"), "tooltip": () => translate("Do not add treasures to 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", "initOrder": 1000 }, "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", "initOrder": 1000 }, "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, "initOrder": 1000 }, "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, "initOrder": 1000 }, "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, "initOrder": 1000 }, "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); } }, "initOrder": 1000 }, } ); /** * For setting up arbitrary GUI objects. */ var g_MiscControls = { "chatPanel": { "hidden": () => { if (!g_IsNetworked) return true; let size = Engine.GetGUIObjectByName("chatPanel").getComputedSize(); return size.right - size.left < g_MinChatWidth; }, }, "chatInput": { "tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete player names 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, "onPress": () => function() { if (g_IsController) launchGame(); else toggleReady(); }, "onPressRight": () => function() { if (!g_IsController && g_IsReady) setReady(0, true); }, "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_GameStarted && ( !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, }, "civInfoButton": { "tooltip": () => sprintf( translate("%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), { "hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"), "hotkey_structree": colorizeHotkey("%(hotkey)s", "structree") }), "onPress": () => function() { Engine.PushGuiPage(g_CivInfo.page, { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" }); } }, "civResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, "teamResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, "lobbyButton": { "onPress": () => function() { if (Engine.HasXmppClient()) Engine.PushGuiPage("page_lobby.xml", { "dialog": true }); }, "hidden": () => !Engine.HasXmppClient() }, "spTips": { "hidden": () => { let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); let spTips = Engine.GetGUIObjectByName("spTips"); return g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true" || spTips.size.right > settingsPanel.getComputedSize().left; } } }; /** * Contains gui elements that are repeated for every player. */ var g_PlayerMiscElements = { "playerBox": { "size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "), }, "playerName": { "caption": (playerIdx) => { let pData = g_GameAttributes.settings.PlayerData[playerIdx]; let assignedGUID = Object.keys(g_PlayerAssignments).find( guid => g_PlayerAssignments[guid].player == playerIdx + 1); let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name); if (g_IsNetworked) name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color); return name; }, }, "playerColor": { "sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color, 100), }, "playerConfig": { "hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI, "onPress": (playerIdx) => function() { openAIConfig(playerIdx); }, "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), { "description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx]) }), }, }; /** * 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; } 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) saveSettingAndWriteToUserConfig("playername.singleplayer", singleplayerName()); initDefaults(); supplementDefaults(); setTimeout(displayGamestateNotifications, 1000); } function initDefaults() { // Remove gaia from both arrays g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1)); let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"); let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"); // 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[i].AIDiff = aiDifficulty; g_DefaultPlayerData[i].AIBehavior = aiBehavior; } deepfreeze(g_DefaultPlayerData); } /** * Sets default values for all g_GameAttribute settings which don't have a value set. */ function supplementDefaults() { g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions || g_VictoryConditions.filter(victoryCondition => !!victoryCondition.Default).map(victoryCondition => victoryCondition.Name); 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() { initSettingObjects(); initSettingsTabButtons(); 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(); } /** * @param {number} dt - Time in milliseconds since last call. */ function slideSettingsPanel(dt) { let slideSpeed = Engine.ConfigDB_GetValue("user", "gui.gamesetup.settingsslide") == "true" ? g_SlideSpeed : Infinity; let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); let rightBorder = Engine.GetGUIObjectByName("settingTabButtons").size.left; let offset = 0; if (g_TabCategorySelected === undefined) { let maxOffset = rightBorder - settingsPanel.size.left; if (maxOffset > 0) offset = Math.min(slideSpeed * dt, maxOffset); } else if (rightBorder > settingsPanel.size.right) offset = Math.min(slideSpeed * dt, rightBorder - settingsPanel.size.right); else { let maxOffset = settingsPanel.size.right - rightBorder; if (maxOffset > 0) offset = -Math.min(slideSpeed * dt, maxOffset); } updateSettingsPanelPosition(offset); } /** * Directly change the position of the settingsPanel. * @param {number} offset - Number of pixels the panel needs to move. */ function updateSettingsPanelPosition(offset) { if (!offset) return; let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); let settingsPanelSize = settingsPanel.size; settingsPanelSize.left += offset; settingsPanelSize.right += offset; settingsPanel.size = settingsPanelSize; let settingsBackground = Engine.GetGUIObjectByName("settingsBackground"); let backgroundSize = settingsBackground.size; backgroundSize.left = settingsPanelSize.left; settingsBackground.size = backgroundSize; let chatPanel = Engine.GetGUIObjectByName("chatPanel"); let chatSize = chatPanel.size; chatSize.right = settingsPanelSize.left - g_ChatSettingsMargin; chatPanel.size = chatSize; chatPanel.hidden = g_MiscControls.chatPanel.hidden(); let spTips = Engine.GetGUIObjectByName("spTips"); spTips.hidden = g_MiscControls.spTips.hidden(); } function hideLoadingWindow() { let loadingWindow = Engine.GetGUIObjectByName("loadingWindow"); if (loadingWindow.hidden) return; loadingWindow.hidden = true; Engine.GetGUIObjectByName("setupWindow").hidden = false; if (!Engine.GetGUIObjectByName("chatPanel").hidden) Engine.GetGUIObjectByName("chatInput").focus(); } /** * Settings under the settings tabs use a generic name. * Player settings use custom names. */ function getGUIObjectNameFromSetting(setting) { let idxOffset = 0; for (let category of g_SettingsTabsGUI) { let idx = category.settings.indexOf(setting); if (idx != -1) return [ "setting", g_Dropdowns[setting] ? "Dropdown" : "Checkbox", "[" + (idx + idxOffset) + "]" ]; idxOffset += category.settings.length; } // Assume there is a GUI object with exactly that setting name return [setting, "", ""]; } /** * Initialize all settings dropdowns and checkboxes. */ function initSettingObjects() { // Copy all initOrder values into one object let initOrder = {}; for (let dropdown in g_Dropdowns) initOrder[dropdown] = g_Dropdowns[dropdown].initOrder; for (let checkbox in g_Checkboxes) initOrder[checkbox] = g_Checkboxes[checkbox].initOrder; // Sort the object on initOrder so we can init the settings in an arbitrary order for (let setting of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b])) if (g_Dropdowns[setting]) initDropdown(setting); else if (g_Checkboxes[setting]) initCheckbox(setting); else warn('The setting "' + setting + '" is not defined.'); for (let dropdown in g_PlayerDropdowns) initPlayerDropdowns(dropdown); } function initDropdown(name, playerIdx) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName); dropdown.list = data.labels(playerIdx).map((label, id) => data.colors && data.colors(playerIdx) ? coloredText(label, data.colors(playerIdx)[id]) : label); dropdown.list_data = data.ids(playerIdx); dropdown.onSelectionChange = function() { if (!g_IsController || g_IsInGuiUpdate || !this.list_data[this.selected] || data.hidden && data.hidden(playerIdx) || data.enabled && !data.enabled(playerIdx)) return; data.select(this.selected, playerIdx); supplementDefaults(); updateGameAttributes(); }; if (data.tooltip) dropdown.onHoverChange = function() { this.tooltip = data.tooltip(this.hovered, playerIdx); }; } function initPlayerDropdowns(name) { for (let i = 0; i < g_MaxPlayers; ++i) initDropdown(name, i); } function initCheckbox(name) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); Engine.GetGUIObjectByName(guiName + guiType + 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 initSettingsTabButtons() { for (let tab in g_SettingsTabsGUI) g_SettingsTabsGUI[tab].tooltip = sprintf(translate("Toggle the %(name)s settings tab."), { "name": g_SettingsTabsGUI[tab].label }) + colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab down."), "tab.next") + colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab up."), "tab.prev"); let settingTabButtons = Engine.GetGUIObjectByName("settingTabButtons"); let settingTabButtonsSize = settingTabButtons.size; settingTabButtonsSize.bottom = settingTabButtonsSize.top + g_SettingsTabsGUI.length * (g_TabButtonHeight + g_TabButtonDist); settingTabButtonsSize.right = g_MiscControls.lobbyButton.hidden() ? settingTabButtonsSize.right : Engine.GetGUIObjectByName("lobbyButton").size.left - g_LobbyButtonSpacing; settingTabButtons.size = settingTabButtonsSize; let settingTabButtonsBackground = Engine.GetGUIObjectByName("settingTabButtonsBackground"); settingTabButtonsBackground.size = settingTabButtonsSize; let gameDescription = Engine.GetGUIObjectByName("mapInfoDescriptionFrame"); let gameDescriptionSize = gameDescription.size; gameDescriptionSize.top = settingTabButtonsSize.bottom + 3; gameDescription.size = gameDescriptionSize; if (!g_IsController) { g_TabCategorySelected = undefined; updateSettingsPanelPosition(Engine.GetGUIObjectByName("settingTabButtons").size.left - Engine.GetGUIObjectByName("settingsPanel").size.left); } placeTabButtons( g_SettingsTabsGUI, g_TabButtonHeight, g_TabButtonDist, category => { selectPanel(category == g_TabCategorySelected ? undefined : category); }, () => { updateGUIObjects(); Engine.GetGUIObjectByName("settingsPanel").hidden = false; }); } 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")); } /** * Distribute the currently visible settings over the settings panel. * First calculate the number of columns required, then place the objects. */ function distributeSettings() { let setupWindowSize = Engine.GetGUIObjectByName("setupWindow").getComputedSize(); let columnWidth = Math.min( g_MaxColumnWidth, (setupWindowSize.right - setupWindowSize.left + Engine.GetGUIObjectByName("settingTabButtons").size.left) / 2); let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); let actualSettingsPanelSize = settingsPanel.getComputedSize(); let maxPerColumn = Math.floor((actualSettingsPanelSize.bottom - actualSettingsPanelSize.top) / g_SettingHeight); let childCount = settingsPanel.children.filter(child => !child.hidden).length; let perColumn = childCount / Math.ceil(childCount / maxPerColumn); let yPos = g_SettingDist; let column = 0; let thisColumn = 0; let settingsPanelSize = settingsPanel.size; for (let child of settingsPanel.children) { if (child.hidden) continue; if (thisColumn >= perColumn) { yPos = g_SettingDist; ++column; thisColumn = 0; } let childSize = child.size; child.size = new GUISize( column * columnWidth, yPos, column * columnWidth + columnWidth - 10, yPos + g_SettingHeight - g_SettingDist); yPos += g_SettingHeight; ++thisColumn; } settingsPanelSize.right = settingsPanelSize.left + (column + 1) * columnWidth; settingsPanel.size = settingsPanelSize; } /** * 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, "playerAssignments": g_PlayerAssignments }); } /** * 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 }); if (newGUID != Engine.GetPlayerGUID() && Object.keys(g_PlayerAssignments).length) soundNotification("gamesetup.join"); 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).nick) == -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 biomePreview = g_GameAttributes.settings.Biome && getBiomePreview(map, g_GameAttributes.settings.Biome); if (biomePreview) return biomePreview; 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 maps = []; // TODO: Should verify these are valid maps before adding to list for (let mapFile of listFiles(g_GameAttributes.mapPath, g_GameAttributes.mapType == "random" ? ".json" : ".xml", false)) { if (mapFile.startsWith("_")) continue; let file = g_GameAttributes.mapPath + mapFile; let mapData = loadMapData(file); - if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || [])) + if (!mapData || !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"); } /** * Initialize the dropdowns specific to each map. */ function reloadMapSpecific() { reloadBiomeList(); reloadTriggerDifficulties(); } function reloadBiomeList() { let biomeList; if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes) { if (typeof g_GameAttributes.settings.SupportedBiomes == "string") biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes)); else biomeList = g_Settings.Biomes.filter( biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1); } g_BiomeList = biomeList && prepareForDropdown( [{ "Id": "random", "Title": translateWithContext("biome", "Random"), "Description": translate("Pick a biome at random."), "Color": g_ColorRandom }].concat(biomeList.map(biome => ({ "Id": biome.Id, "Title": biome.Title, "Description": biome.Description, "Color": g_ColorRegular })))); initDropdown("biome"); updateGUIDropdown("biome"); } function reloadTriggerDifficulties() { g_TriggerDifficultyList = undefined; if (!g_GameAttributes.settings.SupportedTriggerDifficulties) return; let triggerDifficultyList; if (g_GameAttributes.settings.SupportedTriggerDifficulties.Values === true) triggerDifficultyList = g_Settings.TriggerDifficulties; else { triggerDifficultyList = g_Settings.TriggerDifficulties.filter( diff => g_GameAttributes.settings.SupportedTriggerDifficulties.Values.indexOf(diff.Name) != -1); if (!triggerDifficultyList.length) return; } g_TriggerDifficultyList = prepareForDropdown( triggerDifficultyList.map(diff => ({ "Id": diff.Difficulty, "Title": diff.Title, "Description": diff.Tooltip, "Default": diff.Name == g_GameAttributes.settings.SupportedTriggerDifficulties.Default }))); initDropdown("triggerDifficulty"); updateGUIDropdown("triggerDifficulty"); } function reloadGameSpeedChoices() { g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1)); initDropdown("gameSpeed"); supplementDefaults(); } 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 data = Engine.ReadJSONFile(settingsFile); if (!data || !data.attributes || !data.attributes.settings) return; if (data.engine_info.engine_version != Engine.GetEngineInfo().engine_version || !hasSameMods(data.engine_info.mods, Engine.GetEngineInfo().mods)) return; g_IsInGuiUpdate = true; let mapName = data.attributes.map || ""; let mapSettings = data.attributes.settings; g_GameAttributes = data.attributes; 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(); reloadMapSpecific(); g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient(); Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled); supplementDefaults(); g_IsInGuiUpdate = false; } function savePersistMatchSettings() { if (g_IsTutorial) return; Engine.WriteJSONFile( g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, { "attributes": // Delete settings if disabled, so that players are not confronted with old settings after enabling the setting again Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {}, "engine_info": Engine.GetEngineInfo() }); } 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] = clone(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", { "dialog": false }); } 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(); let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; slideSettingsPanel(tickLength); } /** * 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", "Biome", "SupportedBiomes", "SupportedTriggerDifficulties", "TriggerDifficulty"]) g_GameAttributes.settings[prop] = undefined; let mapData = loadMapData(name); let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {}; if (g_GameAttributes.mapType != "random") delete g_GameAttributes.settings.Nomad; if (g_GameAttributes.mapType == "scenario") { delete g_GameAttributes.settings.RelicDuration; delete g_GameAttributes.settings.WonderDuration; 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]; reloadMapSpecific(); unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length); supplementDefaults(); } function isControlArrayElementHidden(playerIdx) { return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length; } /** * @param playerIdx - Only specified for dropdown arrays. */ function updateGUIDropdown(name, playerIdx = undefined) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let dropdown = Engine.GetGUIObjectByName(guiName + guiType + 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); if (guiType == "Dropdown") Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true; let indexHidden = isControlArrayElementHidden(playerIdx); let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let hidden = indexHidden || obj.hidden && obj.hidden(playerIdx); let selected = hidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx))); let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx)); dropdown.enabled = g_IsController && enabled; dropdown.hidden = !g_IsController || !enabled || hidden; dropdown.selected = selected; dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : ""; if (frame) frame.hidden = hidden; if (title && obj.title && !indexHidden) title.caption = sprintf(translateWithContext("Title for specific setting", "%(setting)s:"), { "setting": obj.title(playerIdx) }); if (label && !indexHidden) { label.hidden = g_IsController && enabled || hidden; label.caption = selected == -1 ? translateWithContext("settings 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, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx); let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx); let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx); let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx); if (guiType == "Checkbox") Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true; checkbox.checked = checked; checkbox.enabled = g_IsController && 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("%(setting)s:"), { "setting": obj.title() }); } function updateGUIMiscControl(name, playerIdx) { let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name]; let control = Engine.GetGUIObjectByName(name + idxName); if (!control) warn("No GUI object with name '" + name + "'"); let hide = isControlArrayElementHidden(playerIdx); control.hidden = hide; if (hide) return; for (let property in obj) control[property] = obj[property](playerIdx); } function launchGame() { if (!g_IsController) { error("Only host can start game"); return; } if (!g_GameAttributes.map || g_GameStarted) return; // Prevent reseting the readystate or calling this function twice g_GameStarted = true; updateGUIMiscControl("startGame"); savePersistMatchSettings(); // Select random map if (g_GameAttributes.map == "random") selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1))); if (g_GameAttributes.settings.Biome == "random") g_GameAttributes.settings.Biome = pickRandom( typeof g_GameAttributes.settings.SupportedBiomes == "string" ? g_BiomeList.Id.slice(1).filter(biomeID => biomeID.startsWith(g_GameAttributes.settings.SupportedBiomes)) : g_GameAttributes.settings.SupportedBiomes); g_GameAttributes.settings.VictoryScripts = g_GameAttributes.settings.VictoryConditions.reduce( (scripts, victoryConditionName) => scripts.concat(g_VictoryConditions[g_VictoryConditions.map(data => data.Name).indexOf(victoryConditionName)].Scripts.filter(script => scripts.indexOf(script) == -1)), []); g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []); 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_CivData[civ].SelectableInGameSetup)); } 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, "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(); reloadMapSpecific(); reloadGameSpeedChoices(); reloadPlayerAssignmentChoices(); // Hide exceeding dropdowns and checkboxes for (let setting of Engine.GetGUIObjectByName("settingsPanel").children) setting.hidden = true; // Show the relevant ones if (g_TabCategorySelected !== undefined) { for (let name in g_Dropdowns) if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1) updateGUIDropdown(name); for (let name in g_Checkboxes) if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1) updateGUICheckbox(name); } for (let i = 0; i < g_MaxPlayers; ++i) { for (let name in g_PlayerDropdowns) updateGUIDropdown(name, i); for (let name in g_PlayerMiscElements) updateGUIMiscControl(name, i); } for (let name in g_MiscControls) updateGUIMiscControl(name); updateGameDescription(); distributeSettings(); 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", "playerSlot": playerSlot, "id": g_GameAttributes.settings.PlayerData[playerSlot].AI, "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff, "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior }); } /** * 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; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior; updateGameAttributes(); } function reloadPlayerAssignmentChoices() { 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; g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior; // 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 chatInput = Engine.GetGUIObjectByName("chatInput"); let text = chatInput.caption; if (!text.length) return; chatInput.caption = ""; if (!executeNetworkCommand(text)) Engine.SendNetworkChat(text); chatInput.focus(); } 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 coloredText(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 && msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1) soundNotification("nick"); } 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 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": Engine.LobbyGetNick(), "mapName": g_GameAttributes.map, "niceMapName": getMapDisplayName(g_GameAttributes.map), "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default", "mapType": g_GameAttributes.mapType, "victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","), "nbp": clients.connectedPlayers, "maxnbp": g_GameAttributes.settings.PlayerData.length, "players": clients.list, "stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "", "stunPort": g_StunEndpoint ? g_StunEndpoint.port : "", "mods": JSON.stringify(Engine.GetEngineInfo().mods), }; // 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]), []); } function storeCivInfoPage(data) { g_CivInfo.code = data.civ; g_CivInfo.page = data.page; } Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 22520) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 22521) @@ -1,423 +1,434 @@ /* Copyright (C) 2019 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 "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/external_libraries/libsdl.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "scriptinterface/ScriptRuntime.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" #include #include // TODO: what's a good default? perhaps based on map size #define RMS_RUNTIME_SIZE 96 * 1024 * 1024 extern bool IsQuitRequested(); static bool MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } CMapGeneratorWorker::CMapGeneratorWorker() { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end pthread_join(m_WorkerThread, NULL); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } void* CMapGeneratorWorker::RunThread(void *data) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); CMapGeneratorWorker* self = static_cast(data); shared_ptr mapgenRuntime = ScriptInterface::CreateRuntime(g_ScriptRuntime, RMS_RUNTIME_SIZE); // Enable the script to be aborted JS_SetInterruptCallback(mapgenRuntime->m_rt, MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenRuntime); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } SAFE_DELETE(self->m_ScriptInterface); // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. return NULL; } bool CMapGeneratorWorker::Run() { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); m_ScriptInterface->SetCallbackData(static_cast (this)); // Replace RNG with a seeded deterministic function m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); RegisterScriptFunctions(); // Parse settings JS::RootedValue settingsVal(cx); if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") || !m_ScriptInterface->GetProperty(settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); m_MapGenRNG.seed(seed); // Copy settings to global variable JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } void CMapGeneratorWorker::RegisterScriptFunctions() { // VFS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // File loading m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("LoadHeightmapImage"); m_ScriptInterface->RegisterFunction("LoadMapTerrain"); // Progression and profiling m_ScriptInterface->RegisterFunction("SetProgress"); m_ScriptInterface->RegisterFunction("GetMicroseconds"); m_ScriptInterface->RegisterFunction("ExportMap"); // Template functions m_ScriptInterface->RegisterFunction("GetTemplate"); m_ScriptInterface->RegisterFunction("TemplateExists"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates"); // Engine constants // Length of one tile of the terrain grid in metres. // Useful to transform footprint sizes to the tilegrid coordinate system. m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); // Number of impassable tiles at the map border m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return JS_Now(); } shared_ptr CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& name) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy results std::lock_guard lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy data std::lock_guard lock(self->m_WorkerMutex); if (progress >= self->m_Progress) self->m_Progress = progress; else LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress); } CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename) { std::vector heightmap; if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", filename.string8()); return JS::UndefinedValue(); } CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); JSContext* cx = self->m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue returnValue(cx); ToJSVal_vector(cx, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename) { + CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); + JSContext* cx = self->m_ScriptInterface->GetContext(); + JSAutoRequest rq(cx); + if (!VfsFileExists(filename)) - throw PSERROR_File_OpenFailed(); + { + self->m_ScriptInterface->ReportError( + ("Terrain file \"" + filename.string8() + "\" does not exist!").c_str()); + + return JS::UndefinedValue(); + } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) - throw PSERROR_File_InvalidVersion(); + { + self->m_ScriptInterface->ReportError( + ("Could not load terrain file \"" + filename.string8() + "\" too old version!").c_str()); + + return JS::UndefinedValue(); + } // unpack size ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; // unpack heightmap std::vector heightmap; heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); // unpack texture names size_t textureCount = unpacker.UnpackSize(); std::vector textureNames; textureNames.reserve(textureCount); for (size_t i = 0; i < textureCount; ++i) { CStr texturename; unpacker.UnpackString(texturename); textureNames.push_back(texturename); } // unpack texture IDs per tile ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; std::vector tiles; tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); // reorder by patches and store and save texture IDs per tile std::vector textureIDs; for (ssize_t x = 0; x < tilesPerSide; ++x) { size_t patchX = x / PATCH_SIZE; size_t offX = x % PATCH_SIZE; for (ssize_t y = 0; y < tilesPerSide; ++y) { size_t patchY = y / PATCH_SIZE; size_t offY = y % PATCH_SIZE; // m_Priority and m_Tex2Index unused textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } - CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); - JSContext* cx = self->m_ScriptInterface->GetContext(); - JSAutoRequest rq(cx); JS::RootedValue returnValue(cx, JS::ObjectValue(*JS_NewPlainObject(cx))); self->m_ScriptInterface->SetProperty(returnValue, "height", heightmap); self->m_ScriptInterface->SetProperty(returnValue, "textureNames", textureNames); self->m_ScriptInterface->SetProperty(returnValue, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker()) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } shared_ptr CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/graphics/MapReader.cpp =================================================================== --- ps/trunk/source/graphics/MapReader.cpp (revision 22520) +++ ps/trunk/source/graphics/MapReader.cpp (revision 22521) @@ -1,1559 +1,1559 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 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 "MapReader.h" #include "graphics/Camera.h" #include "graphics/CinemaManager.h" #include "graphics/Entity.h" #include "graphics/GameView.h" #include "graphics/MapGenerator.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/PostprocManager.h" #include "renderer/SkyManager.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpCinemaManager.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include CMapReader::CMapReader() : xml_reader(0), m_PatchesPerSide(0), m_MapGen(0) { cur_terrain_tex = 0; // important - resets generator state } // LoadMap: try to load the map from given file; reinitialise the scene to new data if successful void CMapReader::LoadMap(const VfsPath& pathname, JSRuntime* rt, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities) { pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; pSimulation2 = pSimulation2_; pSimContext = pSimContext_; m_PlayerID = playerID_; m_SkipEntities = skipEntities; m_StartingCameraTarget = INVALID_ENTITY; m_ScriptSettings.init(rt, settings); filename_xml = pathname.ChangeExtension(L".xml"); // In some cases (particularly tests) we don't want to bother storing a large // mostly-empty .pmp file, so we let the XML file specify basic terrain instead. // If there's an .xml file and no .pmp, then we're probably in this XML-only mode only_xml = false; if (!VfsFileExists(pathname) && VfsFileExists(filename_xml)) { only_xml = true; } file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp if (!only_xml) { // [25ms] unpacker.Read(pathname, "PSMP"); file_format_version = unpacker.GetVersion(); } // check oldest supported version if (file_format_version < FILE_READ_VERSION) - throw PSERROR_File_InvalidVersion(); + throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!"); // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); // reset post effects if (pPostproc) pPostproc->SetPostEffect(L"default"); // load map or script settings script if (settings.isUndefined()) RegMemFun(this, &CMapReader::LoadScriptSettings, L"CMapReader::LoadScriptSettings", 50); else RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50); // unpack the data if (!only_xml) RegMemFun(this, &CMapReader::UnpackMap, L"CMapReader::UnpackMap", 1200); // read the corresponding XML file RegMemFun(this, &CMapReader::ReadXML, L"CMapReader::ReadXML", 50); // apply terrain data to the world RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5); // read entities RegMemFun(this, &CMapReader::ReadXMLEntities, L"CMapReader::ReadXMLEntities", 5800); // apply misc data to the world RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5); } // LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful void CMapReader::LoadRandomMap(const CStrW& scriptFile, JSRuntime* rt, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, int playerID_) { m_ScriptFile = scriptFile; pSimulation2 = pSimulation2_; pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL; m_ScriptSettings.init(rt, settings); pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; m_PlayerID = playerID_; m_SkipEntities = false; m_StartingCameraTarget = INVALID_ENTITY; // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); only_xml = false; // copy random map settings (before entity creation) RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50); // load map generator with random map script RegMemFun(this, &CMapReader::GenerateMap, L"CMapReader::GenerateMap", 20000); // parse RMS results into terrain structure RegMemFun(this, &CMapReader::ParseTerrain, L"CMapReader::ParseTerrain", 500); // parse RMS results into environment settings RegMemFun(this, &CMapReader::ParseEnvironment, L"CMapReader::ParseEnvironment", 5); // parse RMS results into camera settings RegMemFun(this, &CMapReader::ParseCamera, L"CMapReader::ParseCamera", 5); // apply terrain data to the world RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5); // parse RMS results into entities RegMemFun(this, &CMapReader::ParseEntities, L"CMapReader::ParseEntities", 1000); // apply misc data to the world RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5); } // UnpackMap: unpack the given data from the raw data stream into local variables int CMapReader::UnpackMap() { return UnpackTerrain(); } // UnpackTerrain: unpack the terrain from the end of the input data stream // - data: map size, heightmap, list of textures used by map, texture tile assignments int CMapReader::UnpackTerrain() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; // first call to generator (this is skipped after first call, // i.e. when the loop below was interrupted) if (cur_terrain_tex == 0) { m_PatchesPerSide = (ssize_t)unpacker.UnpackSize(); // unpack heightmap [600us] size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1; m_Heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16)); // unpack # textures num_terrain_tex = unpacker.UnpackSize(); m_TerrainTextures.reserve(num_terrain_tex); } // unpack texture names; find handle for each texture. // interruptible. while (cur_terrain_tex < num_terrain_tex) { CStr texturename; unpacker.UnpackString(texturename); ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled) CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename); m_TerrainTextures.push_back(texentry); cur_terrain_tex++; LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex); } // unpack tile data [3ms] ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE; m_Tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size()); // reset generator state. cur_terrain_tex = 0; return 0; } int CMapReader::ApplyTerrainData() { if (m_PatchesPerSide == 0) { // we'll probably crash when trying to use this map later throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details."); } if (!only_xml) { // initialise the terrain pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]); // setup the textures on the minipatches STileDesc* tileptr = &m_Tiles[0]; for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k]; // can't fail mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index]; mp.Priority = tileptr->m_Priority; tileptr++; } } } } } CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); return 0; } // ApplyData: take all the input data, and rebuild the scene from it int CMapReader::ApplyData() { // copy over the lighting parameters if (pLightEnv) *pLightEnv = m_LightEnv; CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY); if (pGameView && cmpPlayerManager) { // Default to global camera (with constraints) pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus()); // TODO: Starting rotation? CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID)); if (cmpPlayer && cmpPlayer->HasStartingCamera()) { // Use player starting camera CFixedVector3D pos = cmpPlayer->GetStartingCameraPos(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } else if (m_StartingCameraTarget != INVALID_ENTITY) { // Point camera at entity CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget); if (cmpPosition) { CFixedVector3D pos = cmpPosition->GetPosition(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } } } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname) { VfsPath filename_xml = pathname.ChangeExtension(L".xml"); CXeromyces xmb_file; if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK) return PSRETURN_File_ReadFailed; // Define all the relevant elements used in the XML file #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(scenario); EL(scriptsettings); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(root.GetNodeName() == el_scenario); XERO_ITER_EL(root, child) { int child_name = child.GetNodeName(); if (child_name == el_scriptsettings) { m_ScriptSettings = child.GetText(); } } return PSRETURN_OK; } void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("({})", ret); if (m_ScriptSettings.empty()) return; JS::RootedValue scriptSettingsVal(cx); scriptInterface.ParseJSON(m_ScriptSettings, &scriptSettingsVal); scriptInterface.SetProperty(ret, "settings", scriptSettingsVal, false); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Holds various state data while reading maps, so that loading can be // interrupted (e.g. to update the progress display) then later resumed. class CXMLReader { NONCOPYABLE(CXMLReader); public: CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader) : m_MapReader(mapReader), nodes(NULL, 0, NULL) { Init(xml_filename); } CStr ReadScriptSettings(); // read everything except for entities void ReadXML(); // return semantics: see Loader.cpp!LoadFunc. int ProgressiveReadEntities(); private: CXeromyces xmb_file; CMapReader& m_MapReader; int el_entity; int el_tracks; int el_template, el_player; int el_position, el_orientation, el_obstruction; int el_actor; int at_x, at_y, at_z; int at_group, at_group2; int at_angle; int at_uid; int at_seed; XMBElementList nodes; // children of root // loop counters size_t node_idx; size_t entity_idx; // # entities+nonentities processed and total (for progress calc) int completed_jobs, total_jobs; // maximum used entity ID, so we can safely allocate new ones entity_id_t max_uid; void Init(const VfsPath& xml_filename); void ReadTerrain(XMBElement parent); void ReadEnvironment(XMBElement parent); void ReadCamera(XMBElement parent); void ReadPaths(XMBElement parent); void ReadTriggers(XMBElement parent); int ReadEntities(XMBElement parent, double end_time); }; void CXMLReader::Init(const VfsPath& xml_filename) { // must only assign once, so do it here node_idx = entity_idx = 0; if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK) - throw PSERROR_File_ReadFailed(); + throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!"); // define the elements and attributes that are frequently used in the XML file, // so we don't need to do lots of string construction and comparison when // reading the data. // (Needs to be synchronised with the list in CXMLReader - ugh) #define EL(x) el_##x = xmb_file.GetElementID(#x) #define AT(x) at_##x = xmb_file.GetAttributeID(#x) EL(entity); EL(tracks); EL(template); EL(player); EL(position); EL(orientation); EL(obstruction); EL(actor); AT(x); AT(y); AT(z); AT(group); AT(group2); AT(angle); AT(uid); AT(seed); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementString(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); // find out total number of entities+nonentities // (used when calculating progress) completed_jobs = 0; total_jobs = 0; for (XMBElement node : nodes) total_jobs += node.GetChildNodes().size(); // Find the maximum entity ID, so we can safely allocate new IDs without conflicts max_uid = SYSTEM_ENTITY; XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities")); XERO_ITER_EL(ents, ent) { CStr uid = ent.GetAttributes().GetNamedItem(at_uid); max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt()); } } CStr CXMLReader::ReadScriptSettings() { XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementString(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings")); return settings.GetText(); } void CXMLReader::ReadTerrain(XMBElement parent) { #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) AT(patches); AT(texture); AT(priority); AT(height); #undef AT ssize_t patches = 9; CStr texture = "grass1_spring"; int priority = 0; u16 height = 16384; XERO_ITER_ATTR(parent, attr) { if (attr.Name == at_patches) patches = attr.Value.ToInt(); else if (attr.Name == at_texture) texture = attr.Value; else if (attr.Name == at_priority) priority = attr.Value.ToInt(); else if (attr.Name == at_height) height = (u16)attr.Value.ToInt(); } m_MapReader.m_PatchesPerSide = patches; // Load the texture ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled) CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texture); m_MapReader.pTerrain->Initialize(patches, NULL); // Fill the heightmap u16* heightmap = m_MapReader.pTerrain->GetHeightMap(); ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide(); for (ssize_t i = 0; i < SQR(verticesPerSide); ++i) heightmap[i] = height; // Fill the texture map for (ssize_t pz = 0; pz < patches; ++pz) { for (ssize_t px = 0; px < patches; ++px) { CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail for (ssize_t z = 0; z < PATCH_SIZE; ++z) { for (ssize_t x = 0; x < PATCH_SIZE; ++x) { patch->m_MiniPatches[z][x].Tex = texentry; patch->m_MiniPatches[z][x].Priority = priority; } } } } } void CXMLReader::ReadEnvironment(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(posteffect); EL(skyset); EL(suncolor); EL(sunelevation); EL(sunrotation); EL(terrainambientcolor); EL(unitsambientcolor); EL(water); EL(waterbody); EL(type); EL(color); EL(tint); EL(height); EL(waviness); EL(murkiness); EL(windangle); EL(fog); EL(fogcolor); EL(fogfactor); EL(fogthickness); EL(postproc); EL(brightness); EL(contrast); EL(saturation); EL(bloom); AT(r); AT(g); AT(b); #undef AT #undef EL XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_skyset) { if (m_MapReader.pSkyMan) m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8()); } else if (element_name == el_suncolor) { m_MapReader.m_LightEnv.m_SunColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_sunelevation) { m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_sunrotation) { m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_terrainambientcolor) { m_MapReader.m_LightEnv.m_TerrainAmbientColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_unitsambientcolor) { m_MapReader.m_LightEnv.m_UnitsAmbientColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_fog) { XERO_ITER_EL(element, fog) { int element_name = fog.GetNodeName(); if (element_name == el_fogcolor) { XMBAttributeList attrs = fog.GetAttributes(); m_MapReader.m_LightEnv.m_FogColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_fogfactor) { m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat(); } else if (element_name == el_fogthickness) { m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat(); } } } else if (element_name == el_postproc) { XERO_ITER_EL(element, postproc) { int element_name = postproc.GetNodeName(); if (element_name == el_brightness) { m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat(); } else if (element_name == el_contrast) { m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat(); } else if (element_name == el_saturation) { m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat(); } else if (element_name == el_bloom) { m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat(); } else if (element_name == el_posteffect) { if (m_MapReader.pPostproc) m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8()); } } } else if (element_name == el_water) { XERO_ITER_EL(element, waterbody) { ENSURE(waterbody.GetNodeName() == el_waterbody); XERO_ITER_EL(waterbody, waterelement) { int element_name = waterelement.GetNodeName(); if (element_name == el_height) { CmpPtr cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); ENSURE(cmpWaterManager); cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText())); continue; } // The rest are purely graphical effects, and should be ignored if // graphics are disabled if (!m_MapReader.pWaterMan) continue; if (element_name == el_type) { if (waterelement.GetText() == "default") m_MapReader.pWaterMan->m_WaterType = L"ocean"; else m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8(); } #define READ_COLOR(el, out) \ else if (element_name == el) \ { \ XMBAttributeList attrs = waterelement.GetAttributes(); \ out = CColor( \ attrs.GetNamedItem(at_r).ToFloat(), \ attrs.GetNamedItem(at_g).ToFloat(), \ attrs.GetNamedItem(at_b).ToFloat(), \ 1.f); \ } #define READ_FLOAT(el, out) \ else if (element_name == el) \ { \ out = waterelement.GetText().ToFloat(); \ } \ READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor) READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint) READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness) READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness) READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle) #undef READ_FLOAT #undef READ_COLOR else debug_warn(L"Invalid map XML data"); } } } else debug_warn(L"Invalid map XML data"); } m_MapReader.m_LightEnv.CalculateSunDirection(); } void CXMLReader::ReadCamera(XMBElement parent) { // defaults if we don't find player starting camera #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(declination); EL(rotation); EL(position); AT(angle); AT(x); AT(y); AT(z); #undef AT #undef EL float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f); CVector3D translation = CVector3D(100, 150, -100); XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_declination) { declination = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_rotation) { rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_position) { translation = CVector3D( attrs.GetNamedItem(at_x).ToFloat(), attrs.GetNamedItem(at_y).ToFloat(), attrs.GetNamedItem(at_z).ToFloat()); } else debug_warn(L"Invalid map XML data"); } if (m_MapReader.pGameView) { m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination); m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation); m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation); m_MapReader.pGameView->GetCamera()->UpdateFrustum(); } } void CXMLReader::ReadPaths(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(path); EL(rotation); EL(node); EL(position); EL(target); AT(name); AT(timescale); AT(orientation); AT(mode); AT(style); AT(x); AT(y); AT(z); AT(deltatime); #undef EL #undef AT CmpPtr cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); XERO_ITER_EL(parent, element) { int elementName = element.GetNodeName(); if (elementName == el_path) { CCinemaData pathData; XMBAttributeList attrs = element.GetAttributes(); CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8()); pathData.m_Name = pathName; pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale)); pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8(); pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8(); pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8(); TNSpline positionSpline, targetSpline; fixed lastPositionTime = fixed::Zero(); fixed lastTargetTime = fixed::Zero(); XERO_ITER_EL(element, pathChild) { elementName = pathChild.GetNodeName(); attrs = pathChild.GetAttributes(); // Load node data used for spline if (elementName == el_node) { lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); XERO_ITER_EL(pathChild, nodeChild) { elementName = nodeChild.GetNodeName(); attrs = nodeChild.GetAttributes(); if (elementName == el_position) { CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime); lastPositionTime = fixed::Zero(); } else if (elementName == el_rotation) { // TODO: Implement rotation slerp/spline as another object } else if (elementName == el_target) { CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime); lastTargetTime = fixed::Zero(); } else LOGWARNING("Invalid cinematic element for node child"); } } else LOGWARNING("Invalid cinematic element for path child"); } // Construct cinema path with data gathered CCinemaPath path(pathData, positionSpline, targetSpline); if (path.Empty()) { LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8()); return; } if (!cmpCinemaManager) continue; if (!cmpCinemaManager->HasPath(pathName)) cmpCinemaManager->AddPath(path); else LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8()); } else LOGWARNING("Invalid path child with name '%s'", element.GetText()); } } void CXMLReader::ReadTriggers(XMBElement UNUSED(parent)) { } int CXMLReader::ReadEntities(XMBElement parent, double end_time) { XMBElementList entities = parent.GetChildNodes(); ENSURE(m_MapReader.pSimulation2); CSimulation2& sim = *m_MapReader.pSimulation2; CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); while (entity_idx < entities.size()) { // all new state at this scope and below doesn't need to be // wrapped, since we only yield after a complete iteration. XMBElement entity = entities[entity_idx++]; ENSURE(entity.GetNodeName() == el_entity); XMBAttributeList attrs = entity.GetAttributes(); CStr uid = attrs.GetNamedItem(at_uid); ENSURE(!uid.empty()); int EntityUid = uid.ToInt(); CStrW TemplateName; int PlayerID = 0; CFixedVector3D Position; CFixedVector3D Orientation; long Seed = -1; // Obstruction control groups. entity_id_t ControlGroup = INVALID_ENTITY; entity_id_t ControlGroup2 = INVALID_ENTITY; XERO_ITER_EL(entity, setting) { int element_name = setting.GetNodeName(); //