Index: ps/trunk/binaries/data/mods/public/gui/common/color.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/color.js (revision 19350) +++ ps/trunk/binaries/data/mods/public/gui/common/color.js (revision 19351) @@ -1,160 +1,168 @@ /** * Used to highlight hotkeys in tooltip descriptions. */ var g_HotkeyColor = "255 251 131"; /** * Concatenate integer color values to a string (for use in GUI objects) * * @param {Object} color * @param {number} alpha * @returns {string} */ function rgbToGuiColor(color, alpha) { let ret = "0 0 0"; if (color && ("r" in color) && ("g" in color) && ("b" in color)) ret = color.r + " " + color.g + " " + color.b; if (alpha) ret += " " + alpha; return ret; } /** * True if the colors are identical. * * @param {Object} color1 * @param {Object} color2 * @returns {boolean} */ function sameColor(color1, color2) { return color1.r === color2.r && color1.g === color2.g && color1.b === color2.b; } /** * Computes the euclidian distance between the two colors. * The smaller the return value, the close the colors. Zero if identical. * * @param {Object} color1 * @param {Object} color2 * @returns {number} */ function colorDistance(color1, color2) { return Math.sqrt(Math.pow(color2.r - color1.r, 2) + Math.pow(color2.g - color1.g, 2) + Math.pow(color2.b - color1.b, 2)); } /** * Ensure `value` is between 0 and 1. * * @param {number} value * @returns {number} */ function clampColorValue(value) { return Math.abs(1 - Math.abs(value - 1)); } /** * Convert color value from RGB to HSL space. * * @see {@link http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion} * @param {number} r - red * @param {number} g - green * @param {number} b - blue * @returns {Array} */ function rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max == min) h = s = 0; // achromatic else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; } /** * Convert color value from HSL to RGB space. * * @see {@link http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion} * @param {number} h - hueness * @param {number} s - saturation * @param {number} l - lightness * @returns {Array} */ function hslToRgb(h, s, l) { function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } [h, s, l] = [h, s, l].map(clampColorValue); let r, g, b; if (s == 0) r = g = b = l; // achromatic else { let q = l < 0.5 ? l * (1 + s) : l + s - l * s; let p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [r, g, b].map(n => Math.round(n * 255)); } function colorizeHotkey(text, hotkey) { let key = Engine.ConfigDB_GetValue("user", "hotkey." + hotkey); if (!key || key.toLowerCase() == "unused") return ""; return sprintf(text, { "hotkey": "[color=\"" + g_HotkeyColor + "\"]" + "\\[" + key + "]" + "[/color]" }); } /** * The autocomplete hotkey is hardcoded in SDLK_TAB of CInput.cpp, * as we don't want hotkeys interfering with typing text. */ function colorizeAutocompleteHotkey() { return sprintf(translate("Press %(hotkey)s to autocomplete playernames."), { "hotkey": "[color=\"" + g_HotkeyColor + "\"]" + "\\[" + translateWithContext("hotkey", "Tab") + "]" + "[/color]" }); } + +/** + * Adds grey font if savegame/replay is not compatible. + */ +function compatibilityColor(text, isCompatible) +{ + return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]'; +} Index: ps/trunk/binaries/data/mods/public/gui/common/functions_utility_loadsave.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_utility_loadsave.js (revision 19350) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_utility_loadsave.js (revision 19351) @@ -1,113 +1,115 @@ function sortDecreasingDate(a, b) { return b.metadata.time - a.metadata.time; } -function generateLabel(metadata, engineInfo) +function isCompatibleSavegame(metadata, engineInfo) { - let dateTimeString = Engine.FormatMillisecondsIntoDateStringLocal(metadata.time*1000, translate("yyyy-MM-dd HH:mm:ss")); - let dateString = sprintf(translate("\\[%(date)s]"), { "date": dateTimeString }); + return engineInfo && hasSameSavegameVersion(metadata, engineInfo) && + hasSameEngineVersion(metadata, engineInfo) & hasSameMods(metadata, engineInfo); +} - if (engineInfo) - { - if (!hasSameSavegameVersion(metadata, engineInfo) || !hasSameEngineVersion(metadata, engineInfo)) - dateString = "[color=\"red\"]" + dateString + "[/color]"; - else if (!hasSameMods(metadata, engineInfo)) - dateString = "[color=\"orange\"]" + dateString + "[/color]"; - } +function generateSavegameDateString(metadata, engineInfo) +{ + return compatibilityColor( + Engine.FormatMillisecondsIntoDateStringLocal(metadata.time * 1000, translate("yyyy-MM-dd HH:mm:ss")), + isCompatibleSavegame(metadata, engineInfo)); +} +function generateSavegameLabel(metadata, engineInfo) +{ return sprintf( metadata.description ? translate("%(dateString)s %(map)s - %(description)s") : translate("%(dateString)s %(map)s"), { - "dateString": dateString, + "dateString": generateSavegameDateString(metadata, engineInfo), "map": metadata.initAttributes.map, "description": metadata.description || "" } ); } /** * Check the version compatibility between the saved game to be loaded and the engine */ function hasSameSavegameVersion(metadata, engineInfo) { return metadata.version_major == engineInfo.version_major; } /** * Check the version compatibility between the saved game to be loaded and the engine */ function hasSameEngineVersion(metadata, engineInfo) { return metadata.engine_version && metadata.engine_version == engineInfo.engine_version; } /** * Check the mod compatibility between the saved game to be loaded and the engine * * @param metadata {string[]} * @param engineInfo {string[]} * @returns {boolean} */ function hasSameMods(metadata, engineInfo) { if (!metadata.mods || !engineInfo.mods) return false; // Ignore the "user" mod which is loaded for releases but not working-copies let modsA = metadata.mods.filter(mod => mod != "user"); let modsB = engineInfo.mods.filter(mod => mod != "user"); if (modsA.length != modsB.length) return false; // Mods must be loaded in the same order return modsA.every((mod, index) => mod == modsB[index]); } function deleteGame() { let gameSelection = Engine.GetGUIObjectByName("gameSelection"); let gameID = gameSelection.list_data[gameSelection.selected]; if (!gameID) return; if (Engine.HotkeyIsPressed("session.savedgames.noconfirmation")) reallyDeleteGame(gameID); else messageBox( 500, 200, sprintf(translate("\"%(label)s\""), { "label": gameSelection.list[gameSelection.selected] }) + "\n" + translate("Saved game will be permanently deleted, are you sure?"), translate("DELETE"), [translate("No"), translate("Yes")], [null, function(){ reallyDeleteGame(gameID); }] ); } function reallyDeleteGame(gameID) { if (!Engine.DeleteSavedGame(gameID)) error("Could not delete saved game: " + gameID); // Run init again to refresh saved game list init(); } function deleteTooltip() { let deleteTooltip = colorizeHotkey( translate("Delete the selected entry using %(hotkey)s."), "session.savedgames.delete"); if (deleteTooltip) deleteTooltip += colorizeHotkey( "\n" + translate("Hold %(hotkey)s to delete without confirmation."), "session.savedgames.noconfirmation"); return deleteTooltip; } Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19350) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19351) @@ -1,359 +1,351 @@ /** * 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(); /** * 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 = []; /** * 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; /** * Initializes globals, loads replays and displays the list. */ function init(data) { if (!g_Settings) { Engine.SwitchGuiPage("page_pregame.xml"); return; } loadReplays(data && data.replaySelectionData); if (!g_Replays) { Engine.SwitchGuiPage("page_pregame.xml"); return; } initHotkeyTooltips(); displayReplayList(); } /** * 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. */ function loadReplays(replaySelectionData) { g_Replays = Engine.GetReplays(); 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 victory conditions if (replay.attribs.settings.GameType && g_VictoryConditions.indexOf(replay.attribs.settings.GameType) == -1) g_VictoryConditions.push(replay.attribs.settings.GameType); // 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(); g_VictoryConditions.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"; if (!attribs.settings.GameType) attribs.settings.GameType = "conquest"; // 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": greyout(getReplayDateTime(replay), works), - "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works), - "mapNames": greyout(getReplayMapName(replay), works), - "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works), - "durations": greyout(getReplayDuration(replay), works), - "playerNames": greyout(getReplayPlayernames(replay), works) + "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("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 = translateVictoryCondition(replay.attribs.settings.GameType); Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"), { "numberOfPlayers": replay.attribs.settings.PlayerData.length }); 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); Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description; Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory); setMapPreviewImage("sgMapPreview", mapData.preview); } /** - * Adds grey font if replay is not compatible. - */ -function greyout(text, isCompatible) -{ - return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]'; -} - -/** * 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, g_EngineInfo); } /** * 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; } Index: ps/trunk/binaries/data/mods/public/gui/savedgames/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/savedgames/load.js (revision 19350) +++ ps/trunk/binaries/data/mods/public/gui/savedgames/load.js (revision 19351) @@ -1,158 +1,217 @@ var g_SavedGamesMetadata = []; /** * Needed for formatPlayerInfo to show the player civs in the details. */ const g_CivData = loadCivData(); 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"); - let savedGames = Engine.GetSavedGames().sort(sortDecreasingDate); gameSelection.enabled = !!savedGames.length; - if (!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 => { - gameSelection.list = [translate("No saved games found")]; - gameSelection.selected = -1; - return; - } + game.metadata.id = game.id; + return game.metadata; + }); - // Get current game version and loaded mods - let engineInfo = Engine.GetEngineInfo(); + const sortKey = gameSelection.selected_column; + const 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) + }; + }); - g_SavedGamesMetadata = savedGames.map(game => game.metadata); + if (list.length) + list = prepareForDropdown(list); - gameSelection.list = savedGames.map(game => generateLabel(game.metadata, engineInfo)); - gameSelection.list_data = savedGames.map(game => game.id); + 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); - if (gameSelection.selected == -1) + if (gameSelection.selected == -1 && savedGames.length) gameSelection.selected = 0; - else if (gameSelection.selected >= savedGames.length) // happens when deleting the last saved game - gameSelection.selected = savedGames.length - 1; + else if (gameSelection.selected >= g_SavedGamesMetadata.length) // happens when deleting the last saved game + gameSelection.selected = g_SavedGamesMetadata.length - 1; else - selectionChanged(); + gameSelection.selected = g_SavedGamesMetadata.findIndex(metadata => metadata.id == selectedGameId); 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); 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 = translateVictoryCondition(metadata.initAttributes.settings.GameType); let caption = sprintf(translate("Mods: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) }); if (!hasSameMods(metadata, Engine.GetEngineInfo())) caption = "[color=\"orange\"]" + caption + "[/color]"; 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, engineInfo); let sameEngineVersion = hasSameEngineVersion(metadata, engineInfo); let sameSavegameVersion = hasSameSavegameVersion(metadata, engineInfo); if (sameEngineVersion && sameSavegameVersion && sameMods) { reallyLoadGame(gameId); return; } // Version not compatible ... ask for confirmation let message = translate("This saved game may not be compatible:"); if (!sameEngineVersion) { if (metadata.engine_version) message += "\n" + sprintf(translate("It needs 0 A.D. version %(requiredVersion)s, while you are running version %(currentVersion)s."), { "requiredVersion": metadata.engine_version, "currentVersion": engineInfo.engine_version }); else message += "\n" + translate("It needs an older version of 0 A.D."); } if (!sameSavegameVersion) message += "\n" + sprintf(translate("It needs 0 A.D. savegame version %(requiredVersion)s, while you have savegame version %(currentVersion)s."), { "requiredVersion": metadata.version_major, "currentVersion": engineInfo.version_major }); if (!sameMods) { if (!metadata.mods) metadata.mods = []; message += translate("The savegame needs a different set of mods:") + "\n" + sprintf(translate("Required: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) }) + "\n" + sprintf(translate("Active: %(mods)s"), { "mods": engineInfo.mods.join(translate(", ")) }); } message += "\n" + 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/savedgames/load.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/savedgames/load.xml (revision 19350) +++ ps/trunk/binaries/data/mods/public/gui/savedgames/load.xml (revision 19351) @@ -1,82 +1,125 @@