1
1
forked from 0ad/0ad

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.
This commit is contained in:
wraitii 2021-06-02 06:50:16 +00:00
parent 693f296e71
commit 07e44a75a1
10 changed files with 409 additions and 238 deletions

View File

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

View File

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

View File

@ -17,8 +17,8 @@
<!-- Number of players, hostname, gamestart time -->
<object name="sgGame" size="5 5 100%-5 5" type="text" style="ModernLabelText"/>
<!-- Player Names -->
<object name="sgPlayersNames" type="text" style="MapPlayerList"/>
<!-- Player names & mod details -->
<object name="sgPlayersAndMods" type="text" style="MapPlayerList"/>
</object>

View File

@ -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<CStr>& Mod::GetEnabledMods() const
{
return m_EnabledMods;
}
const std::vector<CStr>& Mod::GetIncompatibleMods() const
{
return m_IncompatibleMods;
}
const std::vector<Mod::ModData>& Mod::GetAvailableMods() const
{
return m_AvailableMods;
}
bool Mod::EnableMods(const ScriptInterface& scriptInterface, const std::vector<CStr>& mods, const bool addPublic)
{
m_IncompatibleMods.clear();
m_EnabledMods.clear();
std::unordered_map<CStr, int> 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<ModData>::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<const Mod::ModData*> Mod::GetEnabledModsData() const
{
std::vector<const ModData*> 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<const Mod::ModData*>& modsA, const std::vector<const Mod::ModData*>& modsB)
{
// Mods must be loaded in the same order.
std::vector<const Mod::ModData*>::const_iterator a = modsA.begin();
std::vector<const Mod::ModData*>::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<CStr>& Mod::GetEnabledMods() const
{
return m_EnabledMods;
}
const std::vector<CStr>& Mod::GetIncompatibleMods() const
{
return m_IncompatibleMods;
}
bool Mod::EnableMods(const ScriptInterface& scriptInterface, const std::vector<CStr>& mods, const bool addPublic)
{
m_IncompatibleMods.clear();
m_EnabledMods.clear();
std::unordered_map<CStr, int> 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<CStr> Mod::CheckForIncompatibleMods(const std::vector<CStr>& mods) const
{
std::vector<CStr> 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<std::vector<CStr>> loadedMods;
for (const CStr& mod : m_EnabledMods)
{
if (mod == "mod" || mod == "user")
continue;
std::vector<ModData>::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<CStr>{ 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();
}

View File

@ -32,51 +32,9 @@ public:
// Singleton-like interface.
static Mod& Instance();
const std::vector<CStr>& GetEnabledMods() const;
const std::vector<CStr>& 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<CStr>& 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<CStr> 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<CStr>& GetEnabledMods() const;
const std::vector<CStr>& GetIncompatibleMods() const;
const std::vector<ModData>& 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<CStr>& 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<const Mod::ModData*> GetEnabledModsData() const;
/**
* @return whether the two lists are compatible for replaying / MP play.
*/
static bool AreModsPlayCompatible(const std::vector<const Mod::ModData*>& modsA, const std::vector<const Mod::ModData*>& 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.

View File

@ -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<std::vector<CStr>>& list) const
namespace
{
CStr ModListToString(const std::vector<const Mod::ModData*>& list)
{
CStr text;
for (const std::vector<CStr>& 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<Mod::ModData>& replayMods)
{
ScriptRequest rq(scriptInterface);
std::vector<std::vector<CStr>> replayMods;
Script::GetProperty(rq, attribs, "mods", replayMods);
std::vector<std::vector<CStr>> 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<const Mod::ModData*> 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<std::vector<CStr>> replayMods;
Script::GetProperty(rq, attribs, "mods", replayMods);
std::vector<Mod::ModData> 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<CStr> mods;
for (const std::vector<CStr>& 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);

View File

@ -113,8 +113,6 @@ public:
private:
std::istream* m_Stream;
CStr ModListToString(const std::vector<std::vector<CStr>>& 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);
};

View File

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

View File

@ -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<Mod::ModData>& a, const std::vector<Mod::ModData>& b)
{
std::vector<const Mod::ModData*> 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<CStr>& 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<GetEngineInfo>(rq, "GetEngineInfo");
ScriptFunction::Register<GetAvailableMods>(rq, "GetAvailableMods");
ScriptFunction::Register<&Mod::GetEnabledMods, ModGetter>(rq, "GetEnabledMods");
ScriptFunction::Register<AreModsPlayCompatible>(rq, "AreModsPlayCompatible");
ScriptFunction::Register<HasIncompatibleMods> (rq, "HasIncompatibleMods");
ScriptFunction::Register<&Mod::GetIncompatibleMods, ModGetter>(rq, "GetIncompatibleMods");
ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine");

View File

@ -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<CStr> 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<const Mod::ModData*>;
{
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));
}
}
};