Index: ps/trunk/binaries/data/mods/mod/gui/common/mod.js =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/common/mod.js +++ ps/trunk/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 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 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) }); } Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js @@ -17,7 +17,7 @@ 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 @@ } { - 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 @@ 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,21 +85,52 @@ } this.sgGame.caption = txt; - } - { - let textHeight = this.sgGame.getTextSize().height; + const textHeight = this.sgGame.getTextSize().height; - let sgGameSize = this.sgGame.size; + const sgGameSize = this.sgGame.size; sgGameSize.bottom = textHeight; this.sgGame.size = sgGameSize; - - let sgPlayersNamesSize = this.sgPlayersNames.size; - sgPlayersNamesSize.top = textHeight + 5; - this.sgPlayersNames.size = sgPlayersNamesSize; } - this.sgPlayersNames.caption = formatPlayerInfo(game.players); + { + // 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); + + txt += "\n" + formatPlayerInfo(game.players); + + // 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.lastGame = game; Engine.ProfileStop(); Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml @@ -17,8 +17,8 @@ - - + + Index: ps/trunk/source/ps/Mod.h =================================================================== --- ps/trunk/source/ps/Mod.h +++ ps/trunk/source/ps/Mod.h @@ -32,8 +32,29 @@ // Singleton-like interface. static Mod& Instance(); + /** + * Parsed mod.json data for C++ usage. + * Note that converting to/from JS is lossy. + */ + struct ModData + { + // 'Folder name' of the mod, e.g. 'public' for the main 0 A.D. mod. + CStr m_Pathname; + // "name" property in the mod.json + CStr m_Name; + + 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). @@ -44,27 +65,24 @@ 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], ...] + * 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. */ - JS::Value GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) const; + const ModData* GetModData(const CStr& mod) 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 + * 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. */ - JS::Value GetEngineInfo(const ScriptInterface& scriptInterface) const; + const std::vector GetEnabledModsData() const; /** - * Gets a dictionary of available mods and their complete, parsed mod.json data. + * @return whether the two lists are compatible for replaying / MP play. */ - JS::Value GetAvailableMods(const ScriptRequest& rq) const; - + 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, @@ -76,25 +94,6 @@ void UpdateAvailableMods(const ScriptInterface& scriptInterface); /** - * Parsed mod.json data for C++ usage. - */ - struct ModData - { - // 'Folder name' of the mod, e.g. 'public' for the main 0 A.D. mod. - CStr m_Pathname; - // "name" property in the mod.json - CStr m_Name; - - CStr m_Version; - std::vector m_Dependencies; - - // For convenience when exporting to JS, keep a record of the full file. - CStr m_Text; - }; - -private: - - /** * Checks a list of @a mods and returns the incompatible mods, if any. */ std::vector CheckForIncompatibleMods(const std::vector& mods) const; Index: ps/trunk/source/ps/Mod.cpp =================================================================== --- ps/trunk/source/ps/Mod.cpp +++ ps/trunk/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 @@ 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 @@ 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 @@ } } -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 @@ 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(); -} Index: ps/trunk/source/ps/Replay.h =================================================================== --- ps/trunk/source/ps/Replay.h +++ ps/trunk/source/ps/Replay.h @@ -113,8 +113,6 @@ 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); }; Index: ps/trunk/source/ps/Replay.cpp =================================================================== --- ps/trunk/source/ps/Replay.cpp +++ ps/trunk/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 @@ // 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 @@ 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 @@ { 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 @@ } // 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); Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp +++ ps/trunk/source/ps/SavedGame.cpp @@ -81,7 +81,8 @@ 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); Index: ps/trunk/source/ps/scripting/JSInterface_Mod.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Mod.cpp +++ ps/trunk/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 @@ 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 @@ 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"); Index: ps/trunk/source/ps/tests/test_Mod.h =================================================================== --- ps/trunk/source/ps/tests/test_Mod.h +++ ps/trunk/source/ps/tests/test_Mod.h @@ -92,10 +92,10 @@ 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 @@ 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)); + } } };