Index: ps/trunk/binaries/data/mods/mod/gui/common/mod.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/mod.js (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/common/mod.js (revision 21239)
@@ -0,0 +1,32 @@
+/**
+ * Check the mod compatibility between the saved game to be loaded and the engine
+ */
+function hasSameMods(modsA, modsB)
+{
+ if (!modsA || !modsB || modsA.length != modsB.length)
+ return false;
+ // Mods must be loaded in the same order. 0: modname, 1: modversion
+ return modsA.every((mod, index) => [0, 1].every(i => mod[i] == modsB[index][i]));
+}
+
+/**
+ * Converts a list of mods and their version into a human-readable string.
+ */
+function modsToString(mods)
+{
+ return mods.map(mod => sprintf(translateWithContext("Mod comparison", "%(mod)s (%(version)s)"), {
+ "mod": mod[0],
+ "version": mod[1]
+ })).join(translate(", "));
+}
+
+/**
+ * Convert the required and active mods and their version into a humanreadable translated string.
+ */
+function comparedModsString(required, active)
+{
+ return sprintf(translateWithContext("Mod comparison", "Required: %(mods)s"),
+ { "mods": modsToString(required) }) + "\n" +
+ sprintf(translateWithContext("Mod comparison", "Active: %(mods)s"),
+ { "mods": modsToString(active) });
+}
Property changes on: ps/trunk/binaries/data/mods/mod/gui/common/mod.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
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 21238)
+++ ps/trunk/binaries/data/mods/public/gui/common/functions_utility_loadsave.js (revision 21239)
@@ -1,106 +1,83 @@
function sortDecreasingDate(a, b)
{
return b.metadata.time - a.metadata.time;
}
function isCompatibleSavegame(metadata, engineInfo)
{
- return engineInfo && hasSameEngineVersion(metadata, engineInfo) & hasSameMods(metadata, engineInfo);
+ return engineInfo && hasSameEngineVersion(metadata, engineInfo) & hasSameMods(metadata.mods, engineInfo.mods);
}
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": 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 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 tooltip = colorizeHotkey(
translate("Delete the selected entry using %(hotkey)s."),
"session.savedgames.delete");
if (tooltip)
tooltip += colorizeHotkey(
"\n" + translate("Hold %(hotkey)s to delete without confirmation."),
"session.savedgames.noconfirmation");
return tooltip;
}
Index: ps/trunk/binaries/data/mods/public/gui/loadgame/load.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21238)
+++ ps/trunk/binaries/data/mods/public/gui/loadgame/load.js (revision 21239)
@@ -1,207 +1,202 @@
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;
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()))
+ 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, engineInfo);
+ 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 = translate("This saved game may not be compatible:");
+ let message = "";
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."), {
+ 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 += "\n" + translate("It needs an older version of 0 A.D.");
+ message += translate("This savegame needs an older version of 0 A.D.") + "\n";
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 += translate("This savegame needs a different sequence of mods:") + "\n" +
+ comparedModsString(metadata.mods, engineInfo.mods) + "\n";
}
- message += "\n" + translate("Do you still want to proceed?");
+ 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_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 21238)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 21239)
@@ -1,204 +1,203 @@
/**
* 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)
{
if (!Engine.StartVisualReplay(replayDirectory))
{
warn('Replay "' + escapeText(Engine.GetReplayDirectoryName(replayDirectory)) + '" not found! Please click on reload cache.');
return;
}
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(", ")) });
+ errMsg = translate("This replay needs a different sequence of mods:") + "\n" +
+ comparedModsString(gameMods, g_EngineInfo.mods);
}
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": {
"dialog": false,
"isReplay": true,
"replayDirectory": g_ReplaysFiltered[selected].directory,
"replaySelectionData": createReplaySelectionData(g_ReplaysFiltered[selected].directory)
},
"selectedData": g_SummarySelectedData
});
}
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(Engine.GetReplayDirectoryName(replay.directory)),
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("Could not delete replay!");
// 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 21238)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 21239)
@@ -1,364 +1,364 @@
/**
* 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 = [];
/**
* 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 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 = 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);
+ 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;
}
Index: ps/trunk/source/ps/Mod.cpp
===================================================================
--- ps/trunk/source/ps/Mod.cpp (revision 21238)
+++ ps/trunk/source/ps/Mod.cpp (revision 21239)
@@ -1,101 +1,126 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2018 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 "ps/Mod.h"
#include
#include "lib/file/file_system.h"
#include "lib/file/vfs/vfs.h"
#include "lib/utf8.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
std::vector g_modsLoaded;
CmdLineArgs g_args;
JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, JS_NewPlainObject(cx));
const Paths paths(g_args);
// loop over all possible paths
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
DirectoryNames modDirs;
DirectoryNames modDirsUser;
GetDirectoryEntries(modPath, NULL, &modDirs);
// Sort modDirs so that we can do a fast lookup below
std::sort(modDirs.begin(), modDirs.end());
PIVFS vfs = CreateVfs();
for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter)
{
vfs->Clear();
if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
continue;
CVFSFile modinfo;
if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
continue;
JS::RootedValue json(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json))
continue;
// Valid mod, add it to our structure
JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json);
}
GetDirectoryEntries(modUserPath, NULL, &modDirsUser);
bool dev = InDevelopmentCopy();
for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter)
{
// If we are in a dev copy we do not mount mods in the user mod folder that
// are already present in the mod folder, thus we skip those here.
if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter))
continue;
vfs->Clear();
if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
continue;
CVFSFile modinfo;
if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
continue;
JS::RootedValue json(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json))
continue;
// Valid mod, add it to our structure
JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json);
}
return JS::ObjectValue(*obj);
}
+
+JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface)
+{
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue availableMods(cx, GetAvailableMods(scriptInterface));
+
+ JS::RootedValue ret(cx, JS::ObjectValue(*JS_NewArrayObject(cx, 0)));
+
+ // Index of the created array
+ size_t j = 0;
+ for (size_t i = 0; i < g_modsLoaded.size(); ++i)
+ {
+ // Ignore user and mod mod as they are irrelevant for compatibility checks
+ if (g_modsLoaded[i] == "mod" || g_modsLoaded[i] == "user")
+ continue;
+ CStr version;
+ JS::RootedValue modData(cx);
+ if (scriptInterface.GetProperty(availableMods, g_modsLoaded[i].c_str(), &modData))
+ scriptInterface.GetProperty(modData, "version", version);
+ scriptInterface.SetPropertyInt(ret, j++, std::vector{g_modsLoaded[i], version});
+ }
+ return ret;
+}
Index: ps/trunk/source/ps/Mod.h
===================================================================
--- ps/trunk/source/ps/Mod.h (revision 21238)
+++ ps/trunk/source/ps/Mod.h (revision 21239)
@@ -1,32 +1,33 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2018 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 .
*/
#ifndef INCLUDED_MOD
#define INCLUDED_MOD
#include "ps/CStr.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "scriptinterface/ScriptInterface.h"
extern std::vector g_modsLoaded;
extern CmdLineArgs g_args;
namespace Mod
{
JS::Value GetAvailableMods(const ScriptInterface& scriptInterface);
+ JS::Value GetLoadedModsWithVersions(const ScriptInterface& scriptInterface);
}
#endif // INCLUDED_MOD
Index: ps/trunk/source/ps/Replay.cpp
===================================================================
--- ps/trunk/source/ps/Replay.cpp (revision 21238)
+++ ps/trunk/source/ps/Replay.cpp (revision 21239)
@@ -1,264 +1,268 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2018 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 "Replay.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/file/file_system.h"
#include "lib/res/h_mgr.h"
#include "lib/tex/tex.h"
#include "ps/Game.h"
#include "ps/CLogger.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
+#include "ps/Mod.h"
#include "ps/Util.h"
#include "ps/VisualReplay.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "simulation2/Simulation2.h"
#include "simulation2/helpers/SimulationCommand.h"
#include
#include
CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) :
m_ScriptInterface(scriptInterface), m_Stream(NULL)
{
}
CReplayLogger::~CReplayLogger()
{
delete m_Stream;
}
void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
{
+ JSContext* cx = m_ScriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
// Add timestamp, since the file-modification-date can change
m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr));
// Add engine version and currently loaded mods for sanity checks when replaying
m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version));
- m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded);
+ m_ScriptInterface.SetProperty(attribs, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface)));
m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryName());
debug_printf("Writing replay to %s\n", m_Directory.string8().c_str());
m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
*m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
}
void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
*m_Stream << "turn " << n << " " << turnLength << "\n";
for (SimulationCommand& command : commands)
*m_Stream << "cmd " << command.player << " " << m_ScriptInterface.StringifyJSON(&command.data, false) << "\n";
*m_Stream << "end\n";
m_Stream->flush();
}
void CReplayLogger::Hash(const std::string& hash, bool quick)
{
if (quick)
*m_Stream << "hash-quick " << Hexify(hash) << "\n";
else
*m_Stream << "hash " << Hexify(hash) << "\n";
}
OsPath CReplayLogger::GetDirectory() const
{
return m_Directory;
}
////////////////////////////////////////////////////////////////
CReplayPlayer::CReplayPlayer() :
m_Stream(NULL)
{
}
CReplayPlayer::~CReplayPlayer()
{
delete m_Stream;
}
void CReplayPlayer::Load(const OsPath& path)
{
ENSURE(!m_Stream);
m_Stream = new std::ifstream(OsString(path).c_str());
ENSURE(m_Stream->good());
}
void CReplayPlayer::Replay(bool serializationtest, int rejointestturn, bool ooslog)
{
ENSURE(m_Stream);
new CProfileViewer;
new CProfileManager;
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
g_Game = new CGame(true, false);
if (serializationtest)
g_Game->GetSimulation2()->EnableSerializationTest();
if (rejointestturn > 0)
g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn);
if (ooslog)
g_Game->GetSimulation2()->EnableOOSLog();
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
// Initialise h_mgr so it doesn't crash when emitting sounds
h_mgr_init();
std::vector commands;
u32 turn = 0;
u32 turnLength = 0;
{
JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
std::string type;
while ((*m_Stream >> type).good())
{
if (type == "start")
{
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue attribs(cx);
ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs));
std::vector replayModList;
g_Game->GetSimulation2()->GetScriptInterface().GetProperty(attribs, "mods", replayModList);
for (const CStr& mod : replayModList)
if (mod != "user" && std::find(g_modsLoaded.begin(), g_modsLoaded.end(), mod) == g_modsLoaded.end())
LOGWARNING("The mod '%s' is required by the replay file, but wasn't passed as an argument!", mod);
for (const CStr& mod : g_modsLoaded)
if (mod != "user" && std::find(replayModList.begin(), replayModList.end(), mod) == replayModList.end())
LOGWARNING("The mod '%s' wasn't used when creating this replay file, but was passed as an argument!", mod);
g_Game->StartGame(&attribs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
}
else if (type == "turn")
{
*m_Stream >> turn >> turnLength;
debug_printf("Turn %u (%u)...\n", turn, turnLength);
}
else if (type == "cmd")
{
player_id_t player;
*m_Stream >> player;
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue data(cx);
g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data);
g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true);
commands.emplace_back(SimulationCommand(player, cx, data));
}
else if (type == "hash" || type == "hash-quick")
{
std::string replayHash;
*m_Stream >> replayHash;
bool quick = (type == "hash-quick");
if (turn % 100 == 0)
{
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick);
ENSURE(ok);
std::string hexHash = Hexify(hash);
if (hexHash == replayHash)
debug_printf("hash ok (%s)\n", hexHash.c_str());
else
debug_printf("HASH MISMATCH (%s != %s)\n", hexHash.c_str(), replayHash.c_str());
}
}
else if (type == "end")
{
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
g_Game->GetSimulation2()->Update(turnLength, commands);
commands.clear();
}
g_Profiler.Frame();
if (turn % 20 == 0)
g_ProfileViewer.SaveToFile();
}
else
{
debug_printf("Unrecognised replay token %s\n", type.c_str());
}
}
}
SAFE_DELETE(m_Stream);
g_Profiler2.SaveToFile();
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
ENSURE(ok);
debug_printf("# Final state: %s\n", Hexify(hash).c_str());
timer_DisplayClientTotals();
SAFE_DELETE(g_Game);
// Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when
// it's already destructed.
g_ScriptRuntime.reset();
// Clean up
delete &g_TexMan;
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
}
Index: ps/trunk/source/ps/SavedGame.cpp
===================================================================
--- ps/trunk/source/ps/SavedGame.cpp (revision 21238)
+++ ps/trunk/source/ps/SavedGame.cpp (revision 21239)
@@ -1,304 +1,304 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "SavedGame.h"
#include "graphics/GameView.h"
#include "gui/GUIManager.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/file/archive/archive_zip.h"
#include "i18n/L10n.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Mod.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
// TODO: we ought to check version numbers when loading files
Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone)
{
// Determine the filename to save under
const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d");
const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave");
VfsPath filename;
// Don't make this a static global like NextNumberedFilename expects, because
// that wouldn't work when 'prefix' changes, and because it's not thread-safe
size_t nextSaveNumber = 0;
vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename);
return Save(filename.Filename().string(), description, simulation, guiMetadataClone);
}
Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone)
{
JSContext* cx = simulation.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// Determine the filename to save under
const VfsPath basenameFormat(L"saves/" + name);
const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave");
// ArchiveWriter_Zip can only write to OsPaths, not VfsPaths,
// but we'd like to handle saved games via VFS.
// To avoid potential confusion from writing with non-VFS then
// reading the same file with VFS, we'll just write to a temporary
// non-VFS path and then load and save again via VFS,
// which is kind of a hack.
OsPath tempSaveFileRealPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath));
tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave";
time_t now = time(NULL);
// Construct the serialized state to be saved
std::stringstream simStateStream;
if (!simulation.SerializeState(simStateStream))
WARN_RETURN(ERR::FAIL);
JS::RootedValue metadata(cx);
JS::RootedValue initAttributes(cx, simulation.GetInitAttributes());
simulation.GetScriptInterface().Eval("({})", &metadata);
simulation.GetScriptInterface().SetProperty(metadata, "engine_version", std::string(engine_version));
- simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded);
+ simulation.GetScriptInterface().SetProperty(metadata, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface())));
simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now);
simulation.GetScriptInterface().SetProperty(metadata, "playerID", g_Game->GetPlayerID());
simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes);
JS::RootedValue guiMetadata(cx);
simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata);
// get some camera data
JS::RootedValue cameraMetadata(cx);
simulation.GetScriptInterface().Eval("({})", &cameraMetadata);
simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosX", g_Game->GetView()->GetCameraPosX());
simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosY", g_Game->GetView()->GetCameraPosY());
simulation.GetScriptInterface().SetProperty(cameraMetadata, "PosZ", g_Game->GetView()->GetCameraPosZ());
simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotX", g_Game->GetView()->GetCameraRotX());
simulation.GetScriptInterface().SetProperty(cameraMetadata, "RotY", g_Game->GetView()->GetCameraRotY());
simulation.GetScriptInterface().SetProperty(cameraMetadata, "Zoom", g_Game->GetView()->GetCameraZoom());
simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata);
simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata);
simulation.GetScriptInterface().SetProperty(metadata, "description", description);
std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true);
// Write the saved game as zip file containing the various components
PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
if (!archiveWriter)
WARN_RETURN(ERR::FAIL);
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json"));
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat"));
archiveWriter.reset(); // close the file
WriteBuffer buffer;
CFileInfo tempSaveFile;
WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
buffer.Reserve(tempSaveFile.Size());
WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8());
debug_printf("Saved game to '%s'\n", realPath.string8().c_str());
return INFO::OK;
}
/**
* Helper class for retrieving data from saved game archives
*/
class CGameLoader
{
NONCOPYABLE(CGameLoader);
public:
/**
* @param scriptInterface the ScriptInterface used for loading metadata.
* @param[out] savedState serialized simulation state stored as string of bytes,
* loaded from simulation.dat inside the archive.
*
* Note: We use a different approach for returning the string and the metadata JS::Value.
* We use a pointer for the string to avoid copies (efficiency). We don't use this approach
* for the metadata because it would be error prone with rooting and the stack-based rooting
* types and confusing (a chain of pointers pointing to other pointers).
*/
CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) :
m_ScriptInterface(scriptInterface),
m_Metadata(scriptInterface.GetJSRuntime()),
m_SavedState(savedState)
{
}
static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData)
{
((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile);
}
void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
if (pathname == L"metadata.json")
{
std::string buffer;
buffer.resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size()));
m_ScriptInterface.ParseJSON(buffer, &m_Metadata);
}
else if (pathname == L"simulation.dat" && m_SavedState)
{
m_SavedState->resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size()));
}
}
JS::Value GetMetadata()
{
return m_Metadata.get();
}
private:
const ScriptInterface& m_ScriptInterface;
JS::PersistentRooted m_Metadata;
std::string* m_SavedState;
};
Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState)
{
// Determine the filename to load
const VfsPath basename(L"saves/" + name);
const VfsPath filename = basename.ChangeExtension(L".0adsave");
// Don't crash just because file isn't found, this can happen if the file is deleted from the OS
if (!VfsFileExists(filename))
return ERR::FILE_NOT_FOUND;
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
WARN_RETURN(ERR::FAIL);
CGameLoader loader(scriptInterface, &savedState);
WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
metadata.set(loader.GetMetadata());
return INFO::OK;
}
JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface)
{
TIMER(L"GetSavedGames");
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedObject games(cx, JS_NewArrayObject(cx, 0));
Status err;
VfsPaths pathnames;
err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames);
WARN_IF_ERR(err);
for (size_t i = 0; i < pathnames.size(); ++i)
{
OsPath realPath;
err = g_VFS->GetRealPath(pathnames[i], realPath);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
{
// Triggered by e.g. the file being open in another program
LOGWARNING("Failed to read saved game '%s'", realPath.string8());
continue; // skip this file
}
CGameLoader loader(scriptInterface, NULL);
err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
JS::RootedValue metadata(cx, loader.GetMetadata());
JS::RootedValue game(cx);
scriptInterface.Eval("({})", &game);
scriptInterface.SetProperty(game, "id", pathnames[i].Basename());
scriptInterface.SetProperty(game, "metadata", metadata);
JS_SetElement(cx, games, i, game);
}
return JS::ObjectValue(*games);
}
bool SavedGames::DeleteSavedGame(const std::wstring& name)
{
const VfsPath basename(L"saves/" + name);
const VfsPath filename = basename.ChangeExtension(L".0adsave");
OsPath realpath;
// Make sure it exists in VFS and find its real path
if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK)
return false; // Error
// Remove from VFS
if (g_VFS->RemoveFile(filename) != INFO::OK)
return false; // Error
// Delete actual file
if (wunlink(realpath) != 0)
return false; // Error
// Successfully deleted file
return true;
}
JS::Value SavedGames::GetEngineInfo(const ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue metainfo(cx);
scriptInterface.Eval("({})", &metainfo);
scriptInterface.SetProperty(metainfo, "engine_version", std::string(engine_version));
- scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded);
+ scriptInterface.SetProperty(metainfo, "mods", JS::RootedValue(cx, Mod::GetLoadedModsWithVersions(scriptInterface)));
scriptInterface.FreezeObject(metainfo, true);
return metainfo;
}