Check for and save mod version for savegames and replays

Reviewed by: elexis
Fixes: #4887
Differential Revision: https://code.wildfiregames.com/D955
This was SVN commit r21239.
This commit is contained in:
Imarok 2018-02-17 16:53:14 +00:00
parent 246d57a81d
commit d5807cd59f
9 changed files with 83 additions and 50 deletions

View file

@ -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) });
}

View file

@ -5,7 +5,7 @@ function sortDecreasingDate(a, b)
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)
@ -37,29 +37,6 @@ 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");

View file

@ -114,8 +114,8 @@ function selectionChanged()
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;
@ -133,7 +133,7 @@ function loadGame()
// 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)
@ -143,32 +143,27 @@ function loadGame()
}
// 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,

View file

@ -89,9 +89,8 @@ function displayReplayCompatibilityError(replay)
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
{

View file

@ -352,7 +352,7 @@ function getReplayDuration(replay)
*/
function isReplayCompatible(replay)
{
return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo);
return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods);
}
/**

View file

@ -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,28 @@ JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface)
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<CStr>{g_modsLoaded[i], version});
}
return ret;
}

View file

@ -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 @@ extern CmdLineArgs g_args;
namespace Mod
{
JS::Value GetAvailableMods(const ScriptInterface& scriptInterface);
JS::Value GetLoadedModsWithVersions(const ScriptInterface& scriptInterface);
}
#endif // INCLUDED_MOD

View file

@ -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 @@ CReplayLogger::~CReplayLogger()
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());

View file

@ -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 @@ Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation
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::Value SavedGames::GetEngineInfo(const ScriptInterface& scriptInterface)
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);