Index: binaries/data/mods/mod/gui/common/mod.js =================================================================== --- /dev/null +++ binaries/data/mods/mod/gui/common/mod.js @@ -0,0 +1,41 @@ +/** + * Check the mod compatibility between the saved game to be loaded and the engine + */ +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[0] != "user"); + let modsB = engineInfo.mods.filter(mod => mod[0] != "user"); + + if (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.filter(mod => mod[0] != "mod" && mod[0] != "user" ).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) }); +} Index: binaries/data/mods/public/gui/common/functions_utility_loadsave.js =================================================================== --- binaries/data/mods/public/gui/common/functions_utility_loadsave.js +++ binaries/data/mods/public/gui/common/functions_utility_loadsave.js @@ -37,29 +37,6 @@ 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"); Index: binaries/data/mods/public/gui/loadgame/load.js =================================================================== --- binaries/data/mods/public/gui/loadgame/load.js +++ binaries/data/mods/public/gui/loadgame/load.js @@ -114,7 +114,7 @@ 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(", ")) }); + let caption = sprintf(translate("Mods: %(mods)s"), { "mods": modsToString(metadata.mods) }); if (!hasSameMods(metadata, Engine.GetEngineInfo())) caption = coloredText(caption, "orange"); Engine.GetGUIObjectByName("savedMods").caption = caption; @@ -143,32 +143,26 @@ } // 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."), { + 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, Index: binaries/data/mods/public/gui/replaymenu/replay_actions.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_actions.js +++ binaries/data/mods/public/gui/replaymenu/replay_actions.js @@ -89,9 +89,8 @@ 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 { Index: source/ps/Mod.h =================================================================== --- source/ps/Mod.h +++ source/ps/Mod.h @@ -1,4 +1,4 @@ -/* 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 @@ -28,5 +28,6 @@ namespace Mod { JS::Value GetAvailableMods(const ScriptInterface& scriptInterface); + JS::Value GetLoadedModsWithVersions(const ScriptInterface& scriptInterface); } #endif // INCLUDED_MOD Index: source/ps/Mod.cpp =================================================================== --- source/ps/Mod.cpp +++ source/ps/Mod.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -99,3 +99,23 @@ 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); + scriptInterface.Eval("([])", &ret); + for (size_t i = 0; i < g_modsLoaded.size(); ++i) + { + CStr version; + JS::RootedValue modData(cx); + if (scriptInterface.GetProperty(availableMods, g_modsLoaded[i].c_str(), &modData)) + scriptInterface.GetProperty(modData, "version", version); + scriptInterface.SetPropertyInt(ret, i, std::vector{g_modsLoaded[i], version}); + } + return ret; +} Index: source/ps/Replay.cpp =================================================================== --- source/ps/Replay.cpp +++ source/ps/Replay.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -31,6 +31,7 @@ #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" @@ -53,12 +54,15 @@ 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()); Index: source/ps/SavedGame.cpp =================================================================== --- source/ps/SavedGame.cpp +++ source/ps/SavedGame.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -82,7 +82,7 @@ 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); @@ -296,7 +296,7 @@ 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);