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