Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 19673) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 19674) @@ -1,191 +1,197 @@ /** * Creates the data for restoring selection, order and filters when returning to the replay menu. */ function createReplaySelectionData(selectedDirectory) { let replaySelection = Engine.GetGUIObjectByName("replaySelection"); let dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); let playersFilter = Engine.GetGUIObjectByName("playersFilter"); let mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); let populationFilter = Engine.GetGUIObjectByName("populationFilter"); let durationFilter = Engine.GetGUIObjectByName("durationFilter"); let compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter"); let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter"); let victoryConFilter = Engine.GetGUIObjectByName("victoryConditionFilter"); let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter"); return { "directory": selectedDirectory, "column": replaySelection.selected_column, "columnOrder": replaySelection.selected_column_order, "filters": { "date": dateTimeFilter.list_data[dateTimeFilter.selected], "playernames": playersFilter.caption, "mapName": mapNameFilter.list_data[mapNameFilter.selected], "mapSize": mapSizeFilter.list_data[mapSizeFilter.selected], "popCap": populationFilter.list_data[populationFilter.selected], "duration": durationFilter.list_data[durationFilter.selected], "compatibility": compatibilityFilter.checked, "singleplayer": singleplayerFilter.list_data[singleplayerFilter.selected], "victoryCondition": victoryConFilter.list_data[victoryConFilter.selected], "ratedGames": ratedGamesFilter.selected } }; } /** * Starts the selected visual replay, or shows an error message in case of incompatibility. */ function startReplay() { var selected = Engine.GetGUIObjectByName("replaySelection").selected; if (selected == -1) return; var replay = g_ReplaysFiltered[selected]; if (isReplayCompatible(replay)) reallyStartVisualReplay(replay.directory); else displayReplayCompatibilityError(replay); } /** * Attempts the visual replay, regardless of the compatibility. * * @param replayDirectory {string} */ function reallyStartVisualReplay(replayDirectory) { // TODO: enhancement: restore filter settings and selected replay when returning from the summary screen. Engine.StartVisualReplay(replayDirectory); Engine.SwitchGuiPage("page_loading.xml", { "attribs": Engine.GetReplayAttributes(replayDirectory), "isNetworked": false, "playerAssignments": { "local":{ "name": singleplayerName(), "player": -1 } }, "savedGUIData": "", "isReplay": true, "replaySelectionData": createReplaySelectionData(replayDirectory) }); } /** * Shows an error message stating why the replay is not compatible. * * @param replay {Object} */ function displayReplayCompatibilityError(replay) { var errMsg; if (replayHasSameEngineVersion(replay)) { let gameMods = replay.attribs.mods || []; errMsg = translate("You don't have the same mods active as the replay does.") + "\n"; errMsg += sprintf(translate("Required: %(mods)s"), { "mods": gameMods.join(translate(", ")) }) + "\n"; errMsg += sprintf(translate("Active: %(mods)s"), { "mods": g_EngineInfo.mods.join(translate(", ")) }); } else { errMsg = translate("This replay is not compatible with your version of the game!") + "\n"; errMsg += sprintf(translate("Your version: %(version)s"), { "version": g_EngineInfo.engine_version }) + "\n"; errMsg += sprintf(translate("Required version: %(version)s"), { "version": replay.attribs.engine_version }); } messageBox(500, 200, errMsg, translate("Incompatible replay")); } /** * Opens the summary screen of the given replay, if its data was found in that directory. */ function showReplaySummary() { var selected = Engine.GetGUIObjectByName("replaySelection").selected; if (selected == -1) return; // Load summary screen data from the selected replay directory let simData = Engine.GetReplayMetadata(g_ReplaysFiltered[selected].directory); if (!simData) { messageBox(500, 200, translate("No summary data available."), translate("Error")); return; } Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "isReplay": true, "replayDirectory": g_ReplaysFiltered[selected].directory, "replaySelectionData": createReplaySelectionData(g_ReplaysFiltered[selected].directory) } }); } +function reloadCache() +{ + let selected = Engine.GetGUIObjectByName("replaySelection").selected; + loadReplays(selected > -1 ? createReplaySelectionData(g_ReplaysFiltered[selected].directory) : "", true); +} + /** * Callback. */ function deleteReplayButtonPressed() { if (!Engine.GetGUIObjectByName("deleteReplayButton").enabled) return; if (Engine.HotkeyIsPressed("session.savedgames.noconfirmation")) deleteReplayWithoutConfirmation(); else deleteReplay(); } /** * Shows a confirmation dialog and deletes the selected replay from the disk in case. */ function deleteReplay() { // Get selected replay var selected = Engine.GetGUIObjectByName("replaySelection").selected; if (selected == -1) return; var replay = g_ReplaysFiltered[selected]; messageBox( 500, 200, translate("Are you sure you want to delete this replay permanently?") + "\n" + escapeText(replay.file), translate("Delete replay"), [translate("No"), translate("Yes")], [null, function() { reallyDeleteReplay(replay.directory); }] ); } /** * Attempts to delete the selected replay from the disk. */ function deleteReplayWithoutConfirmation() { var selected = Engine.GetGUIObjectByName("replaySelection").selected; if (selected > -1) reallyDeleteReplay(g_ReplaysFiltered[selected].directory); } /** * Attempts to delete the given replay directory from the disk. * * @param replayDirectory {string} */ function reallyDeleteReplay(replayDirectory) { var replaySelection = Engine.GetGUIObjectByName("replaySelection"); var selectedIndex = replaySelection.selected; if (!Engine.DeleteReplay(replayDirectory)) error(sprintf("Could not delete replay '%(id)s'", { "id": replayDirectory })); // Refresh replay list init(); replaySelection.selected = Math.min(selectedIndex, g_ReplaysFiltered.length - 1); } Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19673) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19674) @@ -1,353 +1,356 @@ /** * 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); + loadReplays(data && data.replaySelectionData, false); 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. + * @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) +function loadReplays(replaySelectionData, compareFiles) { - g_Replays = Engine.GetReplays(); + 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 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": 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 = translateVictoryCondition(replay.attribs.settings.GameType); Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"), { "numberOfPlayers": replay.attribs.settings.PlayerData.length }); Engine.GetGUIObjectByName("replayFilename").caption = escapeText(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); 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, 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/replaymenu/replay_menu.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 19673) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 19674) @@ -1,266 +1,273 @@