From 07e44a75a16095f4534090a6701f9c60ef8ca8cd Mon Sep 17 00:00:00 2001 From: wraitii Date: Wed, 2 Jun 2021 06:50:16 +0000 Subject: [PATCH] Allow mods to say they should be ignored in replay/MP compatibility checks Since it is very non-trivial to determine which mods change checksums and which don't, this relies on modder goodwill (and on verification on our end for signed mods). The declaration is an optional "ignoreInCompatibilityChecks" boolean in mod.json Also rework slightly the MP lobby mod display to always show the host mods in a clear manner. Differential Revision: https://code.wildfiregames.com/D3968 This was SVN commit r25634. --- binaries/data/mods/mod/gui/common/mod.js | 33 ++- .../public/gui/lobby/LobbyPage/GameDetails.js | 70 ++++-- .../gui/lobby/LobbyPage/GameDetails.xml | 4 +- source/ps/Mod.cpp | 217 +++++++++--------- source/ps/Mod.h | 85 ++++--- source/ps/Replay.cpp | 71 +++--- source/ps/Replay.h | 2 - source/ps/SavedGame.cpp | 3 +- source/ps/scripting/JSInterface_Mod.cpp | 115 +++++++++- source/ps/tests/test_Mod.h | 47 +++- 10 files changed, 409 insertions(+), 238 deletions(-) diff --git a/binaries/data/mods/mod/gui/common/mod.js b/binaries/data/mods/mod/gui/common/mod.js index fc1493f274..427dbf8a31 100644 --- a/binaries/data/mods/mod/gui/common/mod.js +++ b/binaries/data/mods/mod/gui/common/mod.js @@ -1,12 +1,26 @@ /** - * Check the mod compatibility between the saved game to be loaded and the engine + * Check the mod compatibility between the saved game to be loaded and the engine. + * This is a wrapper around an engine function to allow mods to to fancier or specific things. */ function hasSameMods(modsA, modsB) { - if (!modsA || !modsB || modsA.length != modsB.length) + if (!modsA || !modsB) 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])); + return Engine.AreModsPlayCompatible(modsA, modsB); +} + +/** + * Print the shorthand identifier of a mod. + */ +function modToString(mod) +{ + // Skip version for play-compatible mods. + if (mod.ignoreInCompatibilityChecks) + return mod.name; + return sprintf(translateWithContext("Mod comparison", "%(mod)s (%(version)s)"), { + "mod": mod.name, + "version": mod.version + }); } /** @@ -14,10 +28,7 @@ function hasSameMods(modsA, modsB) */ function modsToString(mods) { - return mods.map(mod => sprintf(translateWithContext("Mod comparison", "%(mod)s (%(version)s)"), { - "mod": mod[0], - "version": mod[1] - })).join(translate(", ")); + return mods.map(mod => modToString(mod)).join(translate(", ")); } /** @@ -26,7 +37,7 @@ function modsToString(mods) 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) }); + { "mods": modsToString(required) } + ) + "\n" + sprintf(translateWithContext("Mod comparison", "Active: %(mods)s"), + { "mods": modsToString(active) }); } diff --git a/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js b/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js index 770b603201..9f43184f42 100644 --- a/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js +++ b/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js @@ -17,7 +17,7 @@ class GameDetails this.sgMapName = Engine.GetGUIObjectByName("sgMapName"); this.sgGame = Engine.GetGUIObjectByName("sgGame"); - this.sgPlayersNames = Engine.GetGUIObjectByName("sgPlayersNames"); + this.sgPlayersAndMods = Engine.GetGUIObjectByName("sgPlayersAndMods"); this.sgMapSize = Engine.GetGUIObjectByName("sgMapSize"); this.sgMapPreview = Engine.GetGUIObjectByName("sgMapPreview"); this.sgMapDescription = Engine.GetGUIObjectByName("sgMapDescription"); @@ -59,17 +59,10 @@ class GameDetails } { - let txt; - if (game.isCompatible) - txt = - setStringTags(this.VictoryConditionsFormat, this.CaptionTags) + " " + - (stanza.victoryConditions ? - stanza.victoryConditions.split(",").map(translateVictoryCondition).join(this.Comma) : - translateWithContext("victory condition", "Endless Game")); - else - txt = - setStringTags(this.ModsFormat, this.CaptionTags) + " " + - escapeText(modsToString(game.mods, Engine.GetEngineInfo().mods)); + let txt = setStringTags(this.VictoryConditionsFormat, this.CaptionTags) + " " + + (stanza.victoryConditions ? + stanza.victoryConditions.split(",").map(translateVictoryCondition).join(this.Comma) : + translateWithContext("victory condition", "Endless Game")); txt += "\n" + setStringTags(this.MapTypeFormat, this.CaptionTags) + " " + displayData.mapType + @@ -85,10 +78,6 @@ class GameDetails this.playernameArgs.playername = escapeText(stanza.hostUsername); txt += "\n" + sprintf(this.HostFormat, this.playernameArgs); - this.playerCountArgs.current = escapeText(stanza.nbp); - this.playerCountArgs.total = escapeText(stanza.maxnbp); - txt += "\n" + sprintf(this.PlayerCountFormat, this.playerCountArgs); - if (stanza.startTime) { this.gameStartArgs.time = Engine.FormatMillisecondsIntoDateStringLocal(+stanza.startTime * 1000, this.TimeFormat); @@ -96,22 +85,53 @@ class GameDetails } this.sgGame.caption = txt; + + const textHeight = this.sgGame.getTextSize().height; + + const sgGameSize = this.sgGame.size; + sgGameSize.bottom = textHeight; + this.sgGame.size = sgGameSize; } { - let textHeight = this.sgGame.getTextSize().height; + // Player information + this.playerCountArgs.current = escapeText(stanza.nbp); + this.playerCountArgs.total = escapeText(stanza.maxnbp); + let txt = sprintf(this.PlayerCountFormat, this.playerCountArgs); + txt = setStringTags(txt, this.CaptionTags); - let sgGameSize = this.sgGame.size; - sgGameSize.bottom = textHeight; - this.sgGame.size = sgGameSize; + txt += "\n" + formatPlayerInfo(game.players); - let sgPlayersNamesSize = this.sgPlayersNames.size; - sgPlayersNamesSize.top = textHeight + 5; - this.sgPlayersNames.size = sgPlayersNamesSize; + // Mod information + txt += "\n\n" + setStringTags(this.ModsFormat, this.CaptionTags); + if (!game.isCompatible) + txt = setStringTags(coloredText(txt, "red"), { + "tooltip": sprintf(translate("You have some incompatible mods:\n%(details)s"), { + "details": comparedModsString(game.mods, Engine.GetEngineInfo().mods), + }), + }); + + const sortedMods = game.mods; + sortedMods.sort((a, b) => a.ignoreInCompatibilityChecks - b.ignoreInCompatibilityChecks); + for (const mod of sortedMods) + { + let modStr = escapeText(modToString(mod)); + if (mod.ignoreInCompatibilityChecks) + modStr = setStringTags(coloredText(modStr, "180 180 180"), { + "tooltip": translate("This mod does not affect MP compatibility"), + }); + txt += "\n" + modStr; + } + + this.sgPlayersAndMods.caption = txt; + + // Resize the box + const textHeight = this.sgPlayersAndMods.getTextSize().height; + const size = this.sgPlayersAndMods.size; + size.top = this.sgGame.size.bottom + 5; + this.sgPlayersAndMods.size = size; } - this.sgPlayersNames.caption = formatPlayerInfo(game.players); - this.lastGame = game; Engine.ProfileStop(); } diff --git a/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml b/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml index 43092058cd..c0a4a5e692 100644 --- a/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml +++ b/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml @@ -17,8 +17,8 @@ - - + + diff --git a/source/ps/Mod.cpp b/source/ps/Mod.cpp index 3f57317201..69d6415e67 100644 --- a/source/ps/Mod.cpp +++ b/source/ps/Mod.cpp @@ -27,7 +27,6 @@ #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/Profiler2.h" -#include "ps/Pyrogenesis.h" #include "scriptinterface/JSON.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptExceptions.h" @@ -105,13 +104,11 @@ bool ParseModJSON(const ScriptRequest& rq, const PIVFS& vfs, OsPath modsPath, Os if (!Script::ParseJSON(rq, text, &json)) return false; + Script::FromJSVal(rq, json, data); + + // Complete - FromJSVal won't convert everything. data.m_Pathname = utf8_from_wstring(mod.string()); data.m_Text = text; - - if (!Script::GetProperty(rq, json, "version", data.m_Version)) - return false; - if (!Script::GetProperty(rq, json, "name", data.m_Name)) - return false; if (!Script::GetProperty(rq, json, "dependencies", data.m_Dependencies)) return false; return true; @@ -124,6 +121,114 @@ Mod& Mod::Instance() return g_ModInstance; } +const std::vector& Mod::GetEnabledMods() const +{ + return m_EnabledMods; +} + +const std::vector& Mod::GetIncompatibleMods() const +{ + return m_IncompatibleMods; +} + +const std::vector& Mod::GetAvailableMods() const +{ + return m_AvailableMods; +} + +bool Mod::EnableMods(const ScriptInterface& scriptInterface, const std::vector& mods, const bool addPublic) +{ + m_IncompatibleMods.clear(); + m_EnabledMods.clear(); + + std::unordered_map counts; + for (const CStr& mod : mods) + { + // Ignore duplicates. + if (counts.try_emplace(mod, 0).first->second++ > 0) + continue; + m_EnabledMods.emplace_back(mod); + } + + if (addPublic && counts["public"] == 0) + m_EnabledMods.insert(m_EnabledMods.begin(), "public"); + + if (counts["mod"] == 0) + m_EnabledMods.insert(m_EnabledMods.begin(), "mod"); + + UpdateAvailableMods(scriptInterface); + + m_IncompatibleMods = CheckForIncompatibleMods(m_EnabledMods); + + for (const CStr& mod : m_IncompatibleMods) + m_EnabledMods.erase(std::find(m_EnabledMods.begin(), m_EnabledMods.end(), mod)); + + return m_IncompatibleMods.empty(); +} + +const Mod::ModData* Mod::GetModData(const CStr& mod) const +{ + std::vector::const_iterator it = std::find_if(m_AvailableMods.begin(), m_AvailableMods.end(), + [&mod](const ModData& modData) { return modData.m_Pathname == mod; }); + if (it == m_AvailableMods.end()) + return nullptr; + return std::addressof(*it); +} + +const std::vector Mod::GetEnabledModsData() const +{ + std::vector loadedMods; + for (const CStr& mod : m_EnabledMods) + { + if (mod == "mod" || mod == "user") + continue; + + const ModData* data = GetModData(mod); + + // This ought be impossible, but let's handle it anyways since it's not a reason to crash. + if (!data) + { + LOGERROR("Unavailable mod '%s' was enabled.", mod); + continue; + } + + loadedMods.emplace_back(data); + } + return loadedMods; +} + +bool Mod::AreModsPlayCompatible(const std::vector& modsA, const std::vector& modsB) +{ + // Mods must be loaded in the same order. + std::vector::const_iterator a = modsA.begin(); + std::vector::const_iterator b = modsB.begin(); + + while (a != modsA.end() || b != modsB.end()) + { + if (a != modsA.end() && (*a)->m_IgnoreInCompatibilityChecks) + { + ++a; + continue; + } + if (b != modsB.end() && (*b)->m_IgnoreInCompatibilityChecks) + { + ++b; + continue; + } + // If at this point one of the two lists still contains items, the sizes are different -> fail. + if (a == modsA.end() || b == modsB.end()) + return false; + + if ((*a)->m_Pathname != (*b)->m_Pathname) + return false; + if ((*a)->m_Version != (*b)->m_Version) + return false; + ++a; + ++b; + } + return true; +} + void Mod::UpdateAvailableMods(const ScriptInterface& scriptInterface) { PROFILE2("UpdateAvailableMods"); @@ -169,46 +274,6 @@ void Mod::UpdateAvailableMods(const ScriptInterface& scriptInterface) } } -const std::vector& Mod::GetEnabledMods() const -{ - return m_EnabledMods; -} - -const std::vector& Mod::GetIncompatibleMods() const -{ - return m_IncompatibleMods; -} - -bool Mod::EnableMods(const ScriptInterface& scriptInterface, const std::vector& mods, const bool addPublic) -{ - m_IncompatibleMods.clear(); - m_EnabledMods.clear(); - - std::unordered_map counts; - for (const CStr& mod : mods) - { - // Ignore duplicates. - if (counts.try_emplace(mod, 0).first->second++ > 0) - continue; - m_EnabledMods.emplace_back(mod); - } - - if (addPublic && counts["public"] == 0) - m_EnabledMods.insert(m_EnabledMods.begin(), "public"); - - if (counts["mod"] == 0) - m_EnabledMods.insert(m_EnabledMods.begin(), "mod"); - - UpdateAvailableMods(scriptInterface); - - m_IncompatibleMods = CheckForIncompatibleMods(m_EnabledMods); - - for (const CStr& mod : m_IncompatibleMods) - m_EnabledMods.erase(std::find(m_EnabledMods.begin(), m_EnabledMods.end(), mod)); - - return m_IncompatibleMods.empty(); -} - std::vector Mod::CheckForIncompatibleMods(const std::vector& mods) const { std::vector incompatibleMods; @@ -312,63 +377,3 @@ bool Mod::CompareVersionStrings(const CStr& version, const CStr& op, const CStr& return eq; return versionSize < requiredSize ? lt : gt; } - -JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) const -{ - std::vector> loadedMods; - for (const CStr& mod : m_EnabledMods) - { - if (mod == "mod" || mod == "user") - continue; - - std::vector::const_iterator it = std::find_if(m_AvailableMods.begin(), m_AvailableMods.end(), - [&mod](const ModData& modData) { return modData.m_Pathname == mod; }); - - // This ought be impossible, but let's handle it anyways since it's not a reason to crash. - if (it == m_AvailableMods.end()) - { - LOGERROR("Unavailable mod '%s' was enabled.", mod); - continue; - } - - loadedMods.emplace_back(std::vector{ it->m_Pathname, it->m_Version }); - } - ScriptRequest rq(scriptInterface); - JS::RootedValue returnValue(rq.cx); - Script::ToJSVal(rq, &returnValue, loadedMods); - return returnValue; -} - -JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface) const -{ - ScriptRequest rq(scriptInterface); - - JS::RootedValue mods(rq.cx, GetLoadedModsWithVersions(scriptInterface)); - JS::RootedValue metainfo(rq.cx); - - Script::CreateObject( - rq, - &metainfo, - "engine_version", engine_version, - "mods", mods); - - Script::FreezeObject(rq, metainfo, true); - - return metainfo; -} - -JS::Value Mod::GetAvailableMods(const ScriptRequest& rq) const -{ - JS::RootedValue ret(rq.cx, Script::CreateObject(rq)); - for (const ModData& data : m_AvailableMods) - { - JS::RootedValue json(rq.cx); - if (!Script::ParseJSON(rq, data.m_Text, &json)) - { - ScriptException::Raise(rq, "Error parsing mod.json of '%s'", data.m_Pathname.c_str()); - continue; - } - Script::SetProperty(rq, ret, data.m_Pathname.c_str(), json); - } - return ret.get(); -} diff --git a/source/ps/Mod.h b/source/ps/Mod.h index a7116654c5..738b4d9f7d 100644 --- a/source/ps/Mod.h +++ b/source/ps/Mod.h @@ -32,51 +32,9 @@ public: // Singleton-like interface. static Mod& Instance(); - const std::vector& GetEnabledMods() const; - const std::vector& GetIncompatibleMods() const; - - /** - * Enables specified mods (& mods required by the engine). - * @param addPublic - if true, enable the public mod. - * @return whether the mods were enabled successfully. This can fail if e.g. mods are incompatible. - * If true, GetEnabledMods() should be non-empty, GetIncompatibleMods() empty. Otherwise, GetIncompatibleMods() is non-empty. - */ - bool EnableMods(const ScriptInterface& scriptInterface, const std::vector& mods, const bool addPublic); - - /** - * Get the loaded mods and their version. - * "user" mod and "mod" mod are ignored as they are irrelevant for compatibility checks. - * - * @param scriptInterface the ScriptInterface in which to create the return data. - * @return list of loaded mods with the format [[modA, versionA], [modB, versionB], ...] - */ - JS::Value GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) const; - - /** - * Gets info (version and mods loaded) on the running engine - * - * @param scriptInterface the ScriptInterface in which to create the return data. - * @return list of objects containing data - */ - JS::Value GetEngineInfo(const ScriptInterface& scriptInterface) const; - - /** - * Gets a dictionary of available mods and their complete, parsed mod.json data. - */ - JS::Value GetAvailableMods(const ScriptRequest& rq) const; - - /** - * Fetches available mods and stores some metadata about them. - * This may open the zipped mod archives, depending on the situation, - * and/or try to write files to the user mod folder, - * which can be quite slow, so should be run rarely. - * TODO: if this did not need the scriptInterface to parse JSON, - * we could run it in different contexts and possibly cleaner. - */ - void UpdateAvailableMods(const ScriptInterface& scriptInterface); - /** * Parsed mod.json data for C++ usage. + * Note that converting to/from JS is lossy. */ struct ModData { @@ -87,12 +45,53 @@ public: CStr m_Version; std::vector m_Dependencies; + // If true, the mod is assumed to be 'GUI-only', i.e. ignored for MP or replay compatibility checks. + bool m_IgnoreInCompatibilityChecks; // For convenience when exporting to JS, keep a record of the full file. CStr m_Text; }; + const std::vector& GetEnabledMods() const; + const std::vector& GetIncompatibleMods() const; + const std::vector& GetAvailableMods() const; + + /** + * Enables specified mods (& mods required by the engine). + * @param addPublic - if true, enable the public mod. + * @return whether the mods were enabled successfully. This can fail if e.g. mods are incompatible. + * If true, GetEnabledMods() should be non-empty, GetIncompatibleMods() empty. Otherwise, GetIncompatibleMods() is non-empty. + */ + bool EnableMods(const ScriptInterface& scriptInterface, const std::vector& mods, const bool addPublic); + + /** + * Get data for the given mod. + * @param the mod path name (e.g. 'public') + * @return the mod data or nullptr if unavailable. + * TODO: switch to std::optional or something related. + */ + const ModData* GetModData(const CStr& mod) const; + + /** + * Get a list of the enabled mod's data (intended for compatibility checks). + * "user" mod and "mod" mod are ignored as they are irrelevant for compatibility checks. + */ + const std::vector GetEnabledModsData() const; + + /** + * @return whether the two lists are compatible for replaying / MP play. + */ + static bool AreModsPlayCompatible(const std::vector& modsA, const std::vector& modsB); private: + /** + * Fetches available mods and stores some metadata about them. + * This may open the zipped mod archives, depending on the situation, + * and/or try to write files to the user mod folder, + * which can be quite slow, so should be run rarely. + * TODO: if this did not need the scriptInterface to parse JSON, + * we could run it in different contexts and possibly cleaner. + */ + void UpdateAvailableMods(const ScriptInterface& scriptInterface); /** * Checks a list of @a mods and returns the incompatible mods, if any. diff --git a/source/ps/Replay.cpp b/source/ps/Replay.cpp index 3fb4ac7931..92c8ec7efd 100644 --- a/source/ps/Replay.cpp +++ b/source/ps/Replay.cpp @@ -37,6 +37,7 @@ #include "ps/Mod.h" #include "ps/Util.h" #include "ps/VisualReplay.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" @@ -77,7 +78,8 @@ void CReplayLogger::StartGame(JS::MutableHandleValue attribs) // Add engine version and currently loaded mods for sanity checks when replaying Script::SetProperty(rq, attribs, "engine_version", engine_version); - JS::RootedValue mods(rq.cx, g_Mods.GetLoadedModsWithVersions(m_ScriptInterface)); + JS::RootedValue mods(rq.cx); + Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData()); Script::SetProperty(rq, attribs, "mods", mods); m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath()); @@ -158,46 +160,27 @@ void CReplayPlayer::Load(const OsPath& path) ENSURE(m_Stream->good()); } -CStr CReplayPlayer::ModListToString(const std::vector>& list) const +namespace +{ +CStr ModListToString(const std::vector& list) { CStr text; - for (const std::vector& mod : list) - text += mod[0] + " (" + mod[1] + ")\n"; + for (const Mod::ModData* data : list) + text += data->m_Pathname + " (" + data->m_Version + ")\n"; return text; } -void CReplayPlayer::CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const +void CheckReplayMods(const std::vector& replayMods) { - ScriptRequest rq(scriptInterface); - - std::vector> replayMods; - Script::GetProperty(rq, attribs, "mods", replayMods); - - std::vector> enabledMods; - JS::RootedValue enabledModsJS(rq.cx, g_Mods.GetLoadedModsWithVersions(scriptInterface)); - Script::FromJSVal(rq, enabledModsJS, enabledMods); - - CStr warn; - if (replayMods.size() != enabledMods.size()) - warn = "The number of enabled mods does not match the mods of the replay."; - else - for (size_t i = 0; i < replayMods.size(); ++i) - { - if (replayMods[i][0] != enabledMods[i][0]) - { - warn = "The enabled mods don't match the mods of the replay."; - break; - } - else if (replayMods[i][1] != enabledMods[i][1]) - { - warn = "The mod '" + replayMods[i][0] + "' with version '" + replayMods[i][1] + "' is required by the replay file, but version '" + enabledMods[i][1] + "' is present!"; - break; - } - } - - if (!warn.empty()) - LOGWARNING("%s\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s", warn, ModListToString(replayMods), ModListToString(enabledMods)); + std::vector replayData; + replayData.reserve(replayMods.size()); + for (const Mod::ModData& data : replayMods) + replayData.push_back(&data); + if (!Mod::AreModsPlayCompatible(g_Mods.GetEnabledModsData(), replayData)) + LOGWARNING("Incompatible replay mods detected.\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s", + ModListToString(replayData), ModListToString(g_Mods.GetEnabledModsData())); } +} // anonymous namespace void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick) { @@ -225,7 +208,6 @@ void CReplayPlayer::Replay(const bool serializationtest, const int rejointesttur { std::string attribsStr; { - // TODO: it'd be nice to not create a scriptInterface to load JSON. ScriptInterface scriptInterface("Engine", "Replay", g_ScriptContext); ScriptRequest rq(scriptInterface); std::getline(*m_Stream, attribsStr); @@ -238,18 +220,23 @@ void CReplayPlayer::Replay(const bool serializationtest, const int rejointesttur } // Load the mods specified in the replay. - std::vector> replayMods; - Script::GetProperty(rq, attribs, "mods", replayMods); + std::vector replayMods; + if (!Script::GetProperty(rq, attribs, "mods", replayMods)) + { + LOGERROR("Could not get replay mod information."); + // TODO: do something cleverer than crashing. + ENSURE(false); + } + std::vector mods; - for (const std::vector& ModAndVersion : replayMods) - if (!ModAndVersion.empty()) - mods.emplace_back(ModAndVersion[0]); + for (const Mod::ModData& data : replayMods) + mods.emplace_back(data.m_Pathname); // Ignore the return value, we check below. g_Mods.EnableMods(scriptInterface, mods, false); - MountMods(Paths(g_CmdLineArgs), g_Mods.GetEnabledMods()); + CheckReplayMods(replayMods); - CheckReplayMods(scriptInterface, attribs); + MountMods(Paths(g_CmdLineArgs), g_Mods.GetEnabledMods()); } g_Game = new CGame(false); diff --git a/source/ps/Replay.h b/source/ps/Replay.h index a5590ae1d5..646f84efc9 100644 --- a/source/ps/Replay.h +++ b/source/ps/Replay.h @@ -113,8 +113,6 @@ public: private: std::istream* m_Stream; - CStr ModListToString(const std::vector>& list) const; - void CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const; void TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick); }; diff --git a/source/ps/SavedGame.cpp b/source/ps/SavedGame.cpp index 5f9c6ae877..68c0a87296 100644 --- a/source/ps/SavedGame.cpp +++ b/source/ps/SavedGame.cpp @@ -81,7 +81,8 @@ Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation WARN_RETURN(ERR::FAIL); JS::RootedValue initAttributes(rq.cx, simulation.GetInitAttributes()); - JS::RootedValue mods(rq.cx, g_Mods.GetLoadedModsWithVersions(simulation.GetScriptInterface())); + JS::RootedValue mods(rq.cx); + Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData()); JS::RootedValue metadata(rq.cx); diff --git a/source/ps/scripting/JSInterface_Mod.cpp b/source/ps/scripting/JSInterface_Mod.cpp index b1409a563c..e7c6199af6 100644 --- a/source/ps/scripting/JSInterface_Mod.cpp +++ b/source/ps/scripting/JSInterface_Mod.cpp @@ -20,10 +20,73 @@ #include "JSInterface_Mod.h" #include "ps/Mod.h" +#include "ps/Pyrogenesis.h" #include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/ScriptConversions.h" extern void RestartEngine(); +// To avoid copying data needlessly in GetEngineInfo, implement a ToJSVal for pointer types. +using ModDataCPtr = const Mod::ModData*; + +template<> +void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const ModDataCPtr& data) +{ + ret.set(Script::CreateObject(rq)); + Script::SetProperty(rq, ret, "mod", data->m_Pathname); + Script::SetProperty(rq, ret, "name", data->m_Name); + Script::SetProperty(rq, ret, "version", data->m_Version); + Script::SetProperty(rq, ret, "ignoreInCompatibilityChecks", data->m_IgnoreInCompatibilityChecks); +} + +// Required by JSVAL_VECTOR, but can't be implemented. +template<> +bool Script::FromJSVal(const ScriptRequest &, const JS::HandleValue, ModDataCPtr&) +{ + LOGERROR("Not implemented"); + return false; +} + +JSVAL_VECTOR(const Mod::ModData*); + +// Implement FromJSVal as a non-pointer type. +template<> +void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const Mod::ModData& data) +{ + ret.set(Script::CreateObject(rq)); + Script::SetProperty(rq, ret, "mod", data.m_Pathname); + Script::SetProperty(rq, ret, "name", data.m_Name); + Script::SetProperty(rq, ret, "version", data.m_Version); + Script::SetProperty(rq, ret, "ignoreInCompatibilityChecks", data.m_IgnoreInCompatibilityChecks); +} + +template<> +bool Script::FromJSVal(const ScriptRequest& rq, const JS::HandleValue val, Mod::ModData& data) +{ + // This property is not set in mod.json files, so don't fail if it's not there. + if (Script::HasProperty(rq, val, "mod") && !Script::GetProperty(rq, val, "mod", data.m_Pathname)) + return false; + + if (!Script::GetProperty(rq, val, "version", data.m_Version)) + return false; + if (!Script::GetProperty(rq, val, "name", data.m_Name)) + return false; + + // Optional - this makes the mod 'GUI-only'. + if (Script::HasProperty(rq, val, "ignoreInCompatibilityChecks")) + { + if (!Script::GetProperty(rq, val, "ignoreInCompatibilityChecks", data.m_IgnoreInCompatibilityChecks)) + return false; + } + else + data.m_IgnoreInCompatibilityChecks = false; + + return true; +} + +JSVAL_VECTOR(Mod::ModData); + namespace JSI_Mod { Mod* ModGetter(const ScriptRequest&, JS::CallArgs&) @@ -31,6 +94,53 @@ Mod* ModGetter(const ScriptRequest&, JS::CallArgs&) return &g_Mods; } +JS::Value GetEngineInfo(const ScriptInterface& scriptInterface) +{ + ScriptRequest rq(scriptInterface); + + JS::RootedValue mods(rq.cx); + Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData()); + JS::RootedValue metainfo(rq.cx); + + Script::CreateObject( + rq, + &metainfo, + "engine_version", engine_version, + "mods", mods); + + Script::FreezeObject(rq, metainfo, true); + + return metainfo; +} + +JS::Value GetAvailableMods(const ScriptRequest& rq) +{ + JS::RootedValue ret(rq.cx, Script::CreateObject(rq)); + for (const Mod::ModData& data : g_Mods.GetAvailableMods()) + { + JS::RootedValue json(rq.cx); + if (!Script::ParseJSON(rq, data.m_Text, &json)) + { + ScriptException::Raise(rq, "Error parsing mod.json of '%s'", data.m_Pathname.c_str()); + continue; + } + Script::SetProperty(rq, ret, data.m_Pathname.c_str(), json); + } + return ret.get(); +} + +bool AreModsPlayCompatible(const std::vector& a, const std::vector& b) +{ + std::vector modsA, modsB; + modsA.reserve(a.size()); + for (const Mod::ModData& mod : a) + modsA.push_back(&mod); + modsB.reserve(b.size()); + for (const Mod::ModData& mod : b) + modsB.push_back(&mod); + return Mod::AreModsPlayCompatible(modsA, modsB); +} + bool SetModsAndRestartEngine(const ScriptInterface& scriptInterface, const std::vector& mods) { if (!g_Mods.EnableMods(scriptInterface, mods, false)) @@ -47,9 +157,10 @@ bool HasIncompatibleMods() void RegisterScriptFunctions(const ScriptRequest& rq) { - ScriptFunction::Register<&Mod::GetEngineInfo, ModGetter>(rq, "GetEngineInfo"); - ScriptFunction::Register<&Mod::GetAvailableMods, ModGetter>(rq, "GetAvailableMods"); + ScriptFunction::Register(rq, "GetEngineInfo"); + ScriptFunction::Register(rq, "GetAvailableMods"); ScriptFunction::Register<&Mod::GetEnabledMods, ModGetter>(rq, "GetEnabledMods"); + ScriptFunction::Register(rq, "AreModsPlayCompatible"); ScriptFunction::Register (rq, "HasIncompatibleMods"); ScriptFunction::Register<&Mod::GetIncompatibleMods, ModGetter>(rq, "GetIncompatibleMods"); ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine"); diff --git a/source/ps/tests/test_Mod.h b/source/ps/tests/test_Mod.h index 2ffae723ca..a2c302dbc2 100644 --- a/source/ps/tests/test_Mod.h +++ b/source/ps/tests/test_Mod.h @@ -92,10 +92,10 @@ public: JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); m_Mods.m_AvailableMods = { - Mod::ModData{ "public", "0ad", "0.0.25", {}, "" }, - Mod::ModData{ "wrong", "wrong", "0.0.1", { "0ad=0.0.24" }, "" }, - Mod::ModData{ "good", "good", "0.0.2", { "0ad=0.0.25" }, "" }, - Mod::ModData{ "good2", "good2", "0.0.4", { "0ad>=0.0.24" }, "" }, + Mod::ModData{ "public", "0ad", "0.0.25", {}, false, "" }, + Mod::ModData{ "wrong", "wrong", "0.0.1", { "0ad=0.0.24" }, false, "" }, + Mod::ModData{ "good", "good", "0.0.2", { "0ad=0.0.25" }, false, "" }, + Mod::ModData{ "good2", "good2", "0.0.4", { "0ad>=0.0.24" }, false, "" }, }; std::vector mods; @@ -128,6 +128,45 @@ public: mods.push_back("public"); mods.push_back("does_not_exist"); TS_ASSERT(!m_Mods.CheckForIncompatibleMods(mods).empty()); + } + void test_play_compatible() + { + Mod::ModData a1 = { "a", "a", "0.0.1", {}, false, "" }; + Mod::ModData a2 = { "a", "a", "0.0.2", {}, false, "" }; + Mod::ModData b = { "b", "b", "0.0.1", {}, false, "" }; + Mod::ModData c = { "c", "c", "0.0.1", {}, true, "" }; + + using ModList = std::vector; + { + ModList l1 = { &a1 }; + ModList l2 = { &a2 }; + TS_ASSERT(!Mod::AreModsPlayCompatible(l1, l2)); + } + { + ModList l1 = { &a1, &b }; + ModList l2 = { &a1, &b, &c }; + TS_ASSERT(Mod::AreModsPlayCompatible(l1, l2)); + } + { + ModList l1 = { &c, &b, &a1 }; + ModList l2 = { &b, &c, &a1 }; + TS_ASSERT(Mod::AreModsPlayCompatible(l1, l2)); + } + { + ModList l1 = { &b, &c, &a1 }; + ModList l2 = { &b, &c, &a2 }; + TS_ASSERT(!Mod::AreModsPlayCompatible(l1, l2)); + } + { + ModList l1 = { &c }; + ModList l2 = {}; + TS_ASSERT(Mod::AreModsPlayCompatible(l1, l2)); + } + { + ModList l1 = {}; + ModList l2 = { &b }; + TS_ASSERT(!Mod::AreModsPlayCompatible(l1, l2)); + } } };