Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 21522) +++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 21523) @@ -1,460 +1,463 @@ /** * Highlights the victory condition in the game-description. */ var g_DescriptionHighlight = "orange"; /** * The rating assigned to lobby players who didn't complete a ranked 1v1 yet. */ var g_DefaultLobbyRating = 1200; /** * XEP-0172 doesn't restrict nicknames, but our lobby policy does. * So use this human readable delimiter to separate buddy names in the config file. */ var g_BuddyListDelimiter = ","; /** * Returns the nickname without the lobby rating. */ function splitRatingFromNick(playerName) { let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName); return { "nick": result ? result[1] : playerName, "rating": result ? +result[2] : "" }; } /** * Array of playernames that the current user has marked as buddies. */ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); /** * Denotes which players are a lobby buddy of the current user. */ var g_BuddySymbol = '•'; /** * Returns map description and preview image or placeholder. */ -function getMapDescriptionAndPreview(mapType, mapName) +function getMapDescriptionAndPreview(mapType, mapName, gameAttributes = undefined) { let mapData; if (mapType == "random" && mapName == "random") mapData = { "settings": { "Description": translate("A randomly selected map.") } }; else if (mapType == "random" && Engine.FileExists(mapName + ".json")) mapData = Engine.ReadJSONFile(mapName + ".json"); else if (Engine.FileExists(mapName + ".xml")) mapData = Engine.LoadMapSettings(mapName + ".xml"); + let mapBiome = gameAttributes && g_Settings.Biomes.find(biome => biome.Id == gameAttributes.settings.Biome); + return deepfreeze({ "description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."), - "preview": mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png" + "preview": mapBiome && mapBiome.Preview ? mapBiome.Preview : + mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png" }); } /** * Sets the mappreview image correctly. * It needs to be cropped as the engine only allows loading square textures. * * @param {string} guiObject * @param {string} filename */ function setMapPreviewImage(guiObject, filename) { Engine.GetGUIObjectByName(guiObject).sprite = "cropped:" + 400 / 512 + "," + 300 / 512 + ":" + "session/icons/mappreview/" + filename; } /** * Returns a formatted string describing the player assignments. * Needs g_CivData to translate! * * @param {object} playerDataArray - As known from gamesetup and simstate. * @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player. * @returns {string} */ function formatPlayerInfo(playerDataArray, playerStates) { let playerDescriptions = {}; let playerIdx = 0; for (let playerData of playerDataArray) { if (playerData == null || playerData.Civ && playerData.Civ == "gaia") continue; ++playerIdx; let teamIdx = playerData.Team; let isAI = playerData.AI && playerData.AI != ""; let playerState = playerStates && playerStates[playerIdx] || playerData.State; let isActive = !playerState || playerState == "active"; let playerDescription; if (isAI) { if (playerData.Civ) { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s, %(state)s)"); } else { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdescription)s, %(state)s)"); } } else { if (playerData.Offline) { // Can only occur in the lobby for now, so no strings with civ needed if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)"); } else { if (playerData.Civ) if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)"); else if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(state)s)"); } } // Sort player descriptions by team if (!playerDescriptions[teamIdx]) playerDescriptions[teamIdx] = []; let playerNick = splitRatingFromNick(playerData.Name).nick; playerDescriptions[teamIdx].push(sprintf(playerDescription, { "playerName": coloredText( (g_Buddies.indexOf(playerNick) != -1 ? g_BuddySymbol + " " : "") + escapeText(playerData.Name), (typeof getPlayerColor == 'function' ? (isAI ? "white" : getPlayerColor(playerNick)) : rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color))), "civ": !playerData.Civ ? translate("Unknown Civilization") : g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ? translate(g_CivData[playerData.Civ].Name) : playerData.Civ, "state": playerState == "defeated" ? translateWithContext("playerstate", "defeated") : translateWithContext("playerstate", "won"), "AIdescription": translateAISettings(playerData) })); } let teams = Object.keys(playerDescriptions); if (teams.indexOf("observer") > -1) teams.splice(teams.indexOf("observer"), 1); let teamDescription = []; // If there are no teams, merge all playersDescriptions if (teams.length == 1) teamDescription.push(playerDescriptions[teams[0]].join("\n")); // If there are teams, merge "Team N:" + playerDescriptions else teamDescription = teams.map(team => { let teamCaption = team == -1 ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); // Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + teamCaption + "[/font]", "playerDescriptions": playerDescriptions[team].join("\n") }); }); if (playerDescriptions.observer) teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]", "playerDescriptions": playerDescriptions.observer.join("\n") })); return teamDescription.join("\n\n"); } /** * Sets an additional map label, map preview image and describes the chosen gamesettings more closely. * * Requires g_GameAttributes and g_VictoryConditions. */ function getGameDescription() { let titles = []; if (!g_GameAttributes.settings.VictoryConditions.length) titles.push({ "label": translateWithContext("victory condition", "Endless Game"), "value": translate("No winner will be determined, even if everyone is defeated.") }); let victoryConditions = g_GameAttributes.settings.VictoryConditions.map(victoryConditionName => g_VictoryConditions.find(victoryCondition => victoryCondition.Name == victoryConditionName) || { "Name": victoryConditionName, "Description": "" }); for (let victoryCondition of victoryConditions.sort((a, b) => a.GUIOrder - b.GUIOrder || (a.Title > b.Title ? 1 : a.Title > b.Title ? -1 : 0))) { let title = translateVictoryCondition(victoryCondition.Name); if (victoryCondition.Name == "wonder") title = sprintf( translatePluralWithContext( "victory condition", "Wonder (%(min)s minute)", "Wonder (%(min)s minutes)", g_GameAttributes.settings.WonderDuration ), { "min": g_GameAttributes.settings.WonderDuration } ); let isCaptureTheRelic = victoryCondition.Name == "capture_the_relic"; if (isCaptureTheRelic) title = sprintf( translatePluralWithContext( "victory condition", "Capture the Relic (%(min)s minute)", "Capture the Relic (%(min)s minutes)", g_GameAttributes.settings.RelicDuration ), { "min": g_GameAttributes.settings.RelicDuration } ); titles.push({ "label": title, "value": victoryCondition.Description }); if (isCaptureTheRelic) titles.push({ "label": translate("Relic Count"), "value": g_GameAttributes.settings.RelicCount }); if (victoryCondition.Name == "regicide") if (g_GameAttributes.settings.RegicideGarrison) titles.push({ "label": translate("Hero Garrison"), "value": translate("Heroes can be garrisoned.") }); else titles.push({ "label": translate("Exposed Heroes"), "value": translate("Heroes cannot be garrisoned and they are vulnerable to raids.") }); } if (g_GameAttributes.settings.RatingEnabled && g_GameAttributes.settings.PlayerData.length == 2) titles.push({ "label": translate("Rated game"), "value": translate("When the winner of this match is determined, the lobby score will be adapted.") }); if (g_GameAttributes.settings.LockTeams) titles.push({ "label": translate("Locked Teams"), "value": translate("Players can't change the initial teams.") }); else titles.push({ "label": translate("Diplomacy"), "value": translate("Players can make alliances and declare war on allies.") }); if (g_GameAttributes.settings.LastManStanding) titles.push({ "label": translate("Last Man Standing"), "value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.") }); else titles.push({ "label": translate("Allied Victory"), "value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.") }); titles.push({ "label": translate("Ceasefire"), "value": g_GameAttributes.settings.Ceasefire == 0 ? translate("disabled") : sprintf(translatePlural( "For the first minute, other players will stay neutral.", "For the first %(min)s minutes, other players will stay neutral.", g_GameAttributes.settings.Ceasefire), { "min": g_GameAttributes.settings.Ceasefire }) }); if (g_GameAttributes.map == "random") titles.push({ "label": translateWithContext("Map Selection", "Random Map"), "value": translate("Randomly select a map from the list.") }); else { titles.push({ "label": translate("Map Name"), "value": translate(g_GameAttributes.settings.Name) }); titles.push({ "label": translate("Map Description"), "value": g_GameAttributes.settings.Description ? translate(g_GameAttributes.settings.Description) : translate("Sorry, no description available.") }); } titles.push({ "label": translate("Map Type"), "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); if (typeof g_MapFilterList !== "undefined") titles.push({ "label": translate("Map Filter"), "value": g_MapFilterList.name[g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter)] }); if (g_GameAttributes.mapType == "random") { let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)]; if (mapSize) titles.push({ "label": translate("Map Size"), "value": mapSize }); } if (g_GameAttributes.settings.Biome) { let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome); titles.push({ "label": biome ? biome.Title : translateWithContext("biome", "Random Biome"), "value": biome ? biome.Description : translate("Randomly select a biome from the list.") }); } titles.push({ "label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"), "value": g_GameAttributes.settings.Nomad ? translate("Players start with only few units and have to find a suitable place to build their city.") : translate("Players start with a Civic Center.") }); titles.push({ "label": translate("Starting Resources"), "value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { "startingResourcesTitle": g_StartingResources.Title[ g_StartingResources.Resources.indexOf( g_GameAttributes.settings.StartingResources)], "amount": g_GameAttributes.settings.StartingResources }) }); titles.push({ "label": translate("Population Limit"), "value": g_PopulationCapacities.Title[ g_PopulationCapacities.Population.indexOf( g_GameAttributes.settings.PopulationCap)] }); titles.push({ "label": translate("Treasures"), "value": g_GameAttributes.settings.DisableTreasures ? translateWithContext("treasures", "Disabled") : translateWithContext("treasures", "As defined by the map.") }); titles.push({ "label": translate("Revealed Map"), "value": g_GameAttributes.settings.RevealMap }); titles.push({ "label": translate("Explored Map"), "value": g_GameAttributes.settings.ExploreMap }); titles.push({ "label": translate("Cheats"), "value": g_GameAttributes.settings.CheatsEnabled }); return titles.map(title => sprintf(translate("%(label)s %(details)s"), { "label": coloredText(title.label, g_DescriptionHighlight), "details": title.value === true ? translateWithContext("gamesetup option", "enabled") : title.value || translateWithContext("gamesetup option", "disabled") })).join("\n"); } /** * Sets the win/defeat icon to indicate current player's state. * @param {string} state - The current in-game state of the player. * @param {string} imageID - The name of the XML image object to update. */ function setOutcomeIcon(state, imageID) { let image = Engine.GetGUIObjectByName(imageID); if (state == "won") { image.sprite = "stretched:session/icons/victory.png"; image.tooltip = translate("Victorious"); } else if (state == "defeated") { image.sprite = "stretched:session/icons/defeat.png"; image.tooltip = translate("Defeated"); } } function translateAISettings(playerData) { if (!playerData.AI) return ""; return sprintf(translate("%(AIdifficulty)s %(AIbehavior)s %(AIname)s"), { "AIname": translateAIName(playerData.AI), "AIdifficulty": translateAIDifficulty(playerData.AIDiff), "AIbehavior": translateAIBehavior(playerData.AIBehavior), }); } Index: ps/trunk/binaries/data/mods/public/gui/loadgame/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21522) +++ ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21523) @@ -1,203 +1,203 @@ var g_SavedGamesMetadata = []; /** * Needed for formatPlayerInfo to show the player civs in the details. */ const g_CivData = loadCivData(false, false); function init() { let savedGames = Engine.GetSavedGames(); // Get current game version and loaded mods let engineInfo = Engine.GetEngineInfo(); if (Engine.GetGUIObjectByName("compatibilityFilter").checked) savedGames = savedGames.filter(game => isCompatibleSavegame(game.metadata, engineInfo)); let gameSelection = Engine.GetGUIObjectByName("gameSelection"); gameSelection.enabled = !!savedGames.length; Engine.GetGUIObjectByName("gameSelectionFeedback").hidden = !!savedGames.length; let selectedGameId = gameSelection.list_data[gameSelection.selected]; // Save metadata for the detailed view g_SavedGamesMetadata = savedGames.map(game => { game.metadata.id = game.id; return game.metadata; }); let sortKey = gameSelection.selected_column; let sortOrder = gameSelection.selected_column_order; g_SavedGamesMetadata = g_SavedGamesMetadata.sort((a, b) => { let cmpA, cmpB; switch (sortKey) { case 'date': cmpA = +a.time; cmpB = +b.time; break; case 'mapName': cmpA = translate(a.initAttributes.settings.Name); cmpB = translate(b.initAttributes.settings.Name); break; case 'mapType': cmpA = translateMapType(a.initAttributes.mapType); cmpB = translateMapType(b.initAttributes.mapType); break; case 'description': cmpA = a.description; cmpB = b.description; break; } if (cmpA < cmpB) return -sortOrder; else if (cmpA > cmpB) return +sortOrder; return 0; }); let list = g_SavedGamesMetadata.map(metadata => { let isCompatible = isCompatibleSavegame(metadata, engineInfo); return { "date": generateSavegameDateString(metadata, engineInfo), "mapName": compatibilityColor(translate(metadata.initAttributes.settings.Name), isCompatible), "mapType": compatibilityColor(translateMapType(metadata.initAttributes.mapType), isCompatible), "description": compatibilityColor(metadata.description, isCompatible) }; }); if (list.length) list = prepareForDropdown(list); gameSelection.list_date = list.date || []; gameSelection.list_mapName = list.mapName || []; gameSelection.list_mapType = list.mapType || []; gameSelection.list_description = list.description || []; // Change these last, otherwise crash // list strings used in the delete dialog gameSelection.list = g_SavedGamesMetadata.map(metadata => generateSavegameLabel(metadata, engineInfo)); gameSelection.list_data = g_SavedGamesMetadata.map(metadata => metadata.id); let selectedGameIndex = g_SavedGamesMetadata.findIndex(metadata => metadata.id == selectedGameId); if (selectedGameIndex != -1) gameSelection.selected = selectedGameIndex; else if (gameSelection.selected >= g_SavedGamesMetadata.length) // happens when deleting the last saved game gameSelection.selected = g_SavedGamesMetadata.length - 1; else if (gameSelection.selected == -1 && g_SavedGamesMetadata.length) gameSelection.selected = 0; selectionChanged(); Engine.GetGUIObjectByName("deleteGameButton").tooltip = deleteTooltip(); } function selectionChanged() { let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected]; Engine.GetGUIObjectByName("invalidGame").hidden = !!metadata; Engine.GetGUIObjectByName("validGame").hidden = !metadata; Engine.GetGUIObjectByName("loadGameButton").enabled = !!metadata; Engine.GetGUIObjectByName("deleteGameButton").enabled = !!metadata; if (!metadata) return; Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name); - let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map); + let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map, metadata.initAttributes); setMapPreviewImage("savedInfoPreview", mapData.preview); Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1; Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0); Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType); Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size); Engine.GetGUIObjectByName("savedVictory").caption = metadata.initAttributes.settings.VictoryConditions.map(victoryConditionName => translateVictoryCondition(victoryConditionName)).join(translate(", ")); let caption = sprintf(translate("Mods: %(mods)s"), { "mods": modsToString(metadata.mods) }); if (!hasSameMods(metadata.mods, Engine.GetEngineInfo().mods)) caption = coloredText(caption, "orange"); Engine.GetGUIObjectByName("savedMods").caption = caption; Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo( metadata.initAttributes.settings.PlayerData, metadata.gui.states ); } function loadGame() { let gameSelection = Engine.GetGUIObjectByName("gameSelection"); let gameId = gameSelection.list_data[gameSelection.selected]; let metadata = g_SavedGamesMetadata[gameSelection.selected]; // Check compatibility before really loading it let engineInfo = Engine.GetEngineInfo(); let sameMods = hasSameMods(metadata.mods, engineInfo.mods); let sameEngineVersion = hasSameEngineVersion(metadata, engineInfo); if (sameEngineVersion && sameMods) { reallyLoadGame(gameId); return; } // Version not compatible ... ask for confirmation let message = ""; if (!sameEngineVersion) if (metadata.engine_version) message += sprintf(translate("This savegame needs 0 A.D. version %(requiredVersion)s, while you are running version %(currentVersion)s."), { "requiredVersion": metadata.engine_version, "currentVersion": engineInfo.engine_version }) + "\n"; else message += translate("This savegame needs an older version of 0 A.D.") + "\n"; if (!sameMods) { if (!metadata.mods) metadata.mods = []; message += translate("This savegame needs a different sequence of mods:") + "\n" + comparedModsString(metadata.mods, engineInfo.mods) + "\n"; } message += translate("Do you still want to proceed?"); messageBox( 500, 250, message, translate("Warning"), [translate("No"), translate("Yes")], [init, function(){ reallyLoadGame(gameId); }] ); } function reallyLoadGame(gameId) { let metadata = Engine.StartSavedGame(gameId); if (!metadata) { // Probably the file wasn't found // Show error and refresh saved game list error("Could not load saved game: " + gameId); init(); return; } let pData = metadata.initAttributes.settings.PlayerData[metadata.playerID]; Engine.SwitchGuiPage("page_loading.xml", { "attribs": metadata.initAttributes, "isNetworked": false, "playerAssignments": { "local": { "name": pData ? pData.Name : singleplayerName(), "player": metadata.playerID } }, "savedGUIData": metadata.gui }); } Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 21522) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 21523) @@ -1,357 +1,357 @@ /** * Used for checking replay compatibility. */ const g_EngineInfo = Engine.GetEngineInfo(); /** * Needed for formatPlayerInfo to show the player civs in the details. */ const g_CivData = loadCivData(false, false); /** * Used for creating the mapsize filter. */ const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); /** * All replays found in the directory. */ var g_Replays = []; /** * List of replays after applying the display filter. */ var g_ReplaysFiltered = []; /** * Array of unique usernames of all replays. Used for autocompleting usernames. */ var g_Playernames = []; /** * Sorted list of unique maptitles. Used by mapfilter. */ var g_MapNames = []; /** * Sorted list of the victory conditions occuring in the replays */ var g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; /** * Directory name of the currently selected replay. Used to restore the selection after changing filters. */ var g_SelectedReplayDirectory = ""; /** * Skip duplicate expensive GUI updates before init is complete. */ var g_ReplaysLoaded = false; /** * Remember last viewed summary panel and charts. */ var g_SummarySelectedData; /** * Initializes globals, loads replays and displays the list. */ function init(data) { if (!g_Settings) { Engine.SwitchGuiPage("page_pregame.xml"); return; } loadReplays(data && data.replaySelectionData, false); if (!g_Replays) { Engine.SwitchGuiPage("page_pregame.xml"); return; } initHotkeyTooltips(); displayReplayList(); if (data && data.summarySelectedData) g_SummarySelectedData = data.summarySelectedData; } /** * Store the list of replays loaded in C++ in g_Replays. * Check timestamp and compatibility and extract g_Playernames, g_MapNames, g_VictoryConditions. * Restore selected filters and item. * @param replaySelectionData - Currently selected filters and item to be restored after the loading. * @param compareFiles - If true, compares files briefly (which might be slow with optical harddrives), * otherwise blindly trusts the replay cache. */ function loadReplays(replaySelectionData, compareFiles) { g_Replays = Engine.GetReplays(compareFiles); if (!g_Replays) return; g_Playernames = []; for (let replay of g_Replays) { let nonAIPlayers = 0; // Check replay for compatibility replay.isCompatible = isReplayCompatible(replay); sanitizeGameAttributes(replay.attribs); // Extract map names if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "") g_MapNames.push(replay.attribs.settings.Name); // Extract playernames for (let playerData of replay.attribs.settings.PlayerData) { if (!playerData || playerData.AI) continue; // Remove rating from nick let playername = playerData.Name; let ratingStart = playername.indexOf(" ("); if (ratingStart != -1) playername = playername.substr(0, ratingStart); if (g_Playernames.indexOf(playername) == -1) g_Playernames.push(playername); ++nonAIPlayers; } replay.isMultiplayer = nonAIPlayers > 1; replay.isRated = nonAIPlayers == 2 && replay.attribs.settings.PlayerData.length == 2 && replay.attribs.settings.RatingEnabled; } g_MapNames.sort(); // Reload filters (since they depend on g_Replays and its derivatives) initFilters(replaySelectionData && replaySelectionData.filters); // Restore user selection if (replaySelectionData) { if (replaySelectionData.directory) g_SelectedReplayDirectory = replaySelectionData.directory; let replaySelection = Engine.GetGUIObjectByName("replaySelection"); if (replaySelectionData.column) replaySelection.selected_column = replaySelectionData.column; if (replaySelectionData.columnOrder) replaySelection.selected_column_order = replaySelectionData.columnOrder; } g_ReplaysLoaded = true; } /** * We may encounter malformed replays. */ function sanitizeGameAttributes(attribs) { if (!attribs.settings) attribs.settings = {}; if (!attribs.settings.Size) attribs.settings.Size = -1; if (!attribs.settings.Name) attribs.settings.Name = ""; if (!attribs.settings.PlayerData) attribs.settings.PlayerData = []; if (!attribs.settings.PopulationCap) attribs.settings.PopulationCap = 300; if (!attribs.settings.mapType) attribs.settings.mapType = "skirmish"; // Remove gaia if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null) attribs.settings.PlayerData.shift(); attribs.settings.PlayerData.forEach((pData, index) => { if (!pData.Name) pData.Name = ""; }); } function initHotkeyTooltips() { Engine.GetGUIObjectByName("playersFilter").tooltip = translate("Filter replays by typing one or more, partial or complete playernames.") + " " + colorizeAutocompleteHotkey(); Engine.GetGUIObjectByName("deleteReplayButton").tooltip = deleteTooltip(); } /** * Filter g_Replays, fill the GUI list with that data and show the description of the current replay. */ function displayReplayList() { if (!g_ReplaysLoaded) return; // Remember previously selected replay var replaySelection = Engine.GetGUIObjectByName("replaySelection"); if (replaySelection.selected != -1) g_SelectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory; filterReplays(); var list = g_ReplaysFiltered.map(replay => { let works = replay.isCompatible; return { "directories": replay.directory, "months": compatibilityColor(getReplayDateTime(replay), works), "popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works), "mapNames": compatibilityColor(getReplayMapName(replay), works), "mapSizes": compatibilityColor(translateMapSize(replay.attribs.settings.Size), works), "durations": compatibilityColor(getReplayDuration(replay), works), "playerNames": compatibilityColor(getReplayPlayernames(replay), works) }; }); if (list.length) list = prepareForDropdown(list); // Push to GUI replaySelection.selected = -1; replaySelection.list_months = list.months || []; replaySelection.list_players = list.playerNames || []; replaySelection.list_mapName = list.mapNames || []; replaySelection.list_mapSize = list.mapSizes || []; replaySelection.list_popCapacity = list.popCaps || []; replaySelection.list_duration = list.durations || []; // Change these last, otherwise crash replaySelection.list = list.directories || []; replaySelection.list_data = list.directories || []; replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory); displayReplayDetails(); } /** * Shows preview image, description and player text in the right panel. */ function displayReplayDetails() { let selected = Engine.GetGUIObjectByName("replaySelection").selected; let replaySelected = selected > -1; Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; Engine.GetGUIObjectByName("replayFilename").hidden = !replaySelected; Engine.GetGUIObjectByName("summaryButton").hidden = true; if (!replaySelected) return; let replay = g_ReplaysFiltered[selected]; Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name); Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size); Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType); Engine.GetGUIObjectByName("sgVictory").caption = replay.attribs.settings.VictoryConditions.map(victoryConditionName => translateVictoryCondition(victoryConditionName)).join(translate(", ")); Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"), { "numberOfPlayers": replay.attribs.settings.PlayerData.length }); Engine.GetGUIObjectByName("replayFilename").caption = Engine.GetReplayDirectoryName(replay.directory); let metadata = Engine.GetReplayMetadata(replay.directory); Engine.GetGUIObjectByName("sgPlayersNames").caption = formatPlayerInfo( replay.attribs.settings.PlayerData, Engine.GetGUIObjectByName("showSpoiler").checked && metadata && metadata.playerStates && metadata.playerStates.map(pState => pState.state) ); - let mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map); + let mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map, replay.attribs); Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description; Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory); setMapPreviewImage("sgMapPreview", mapData.preview); } /** * Returns a human-readable version of the replay date. */ function getReplayDateTime(replay) { return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM-dd HH:mm")); } /** * Returns a human-readable list of the playernames of that replay. * * @returns {string} */ function getReplayPlayernames(replay) { return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", "); } /** * Returns the name of the map of the given replay. * * @returns {string} */ function getReplayMapName(replay) { return translate(replay.attribs.settings.Name); } /** * Returns the month of the given replay in the format "yyyy-MM". * * @returns {string} */ function getReplayMonth(replay) { return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM")); } /** * Returns a human-readable version of the time when the replay started. * * @returns {string} */ function getReplayDuration(replay) { return timeToString(replay.duration * 1000); } /** * True if we can start the given replay with the currently loaded mods. */ function isReplayCompatible(replay) { return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods); } /** * True if we can start the given replay with the currently loaded mods. */ function replayHasSameEngineVersion(replay) { return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version; }