1
0
forked from 0ad/0ad

Mod data parsing rework

Parses mod.json data not only in temporary JS values, but in a proper
C++ struct.
This will ultimately make it more convenient to pass more than just the
version to JS in D3968, and it enforces the schema a bit more.

Differential Revision: https://code.wildfiregames.com/D3988
This was SVN commit r25546.
This commit is contained in:
wraitii 2021-05-25 06:12:45 +00:00
parent b56f0222d9
commit 498f0d420b
3 changed files with 125 additions and 136 deletions

View File

@ -28,9 +28,10 @@
#include "ps/GameSetup/Paths.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptExceptions.h"
#include "scriptinterface/ScriptInterface.h"
#include <algorithm>
#include <boost/algorithm/string/split.hpp>
@ -46,7 +47,7 @@ namespace
*/
Mod g_ModInstance;
bool ParseModJSON(const ScriptRequest& rq, const PIVFS& vfs, OsPath modsPath, OsPath mod, JS::MutableHandleValue json)
bool LoadModJSON(const PIVFS& vfs, OsPath modsPath, OsPath mod, std::string& text)
{
// Attempt to open mod.json first.
std::ifstream modjson;
@ -66,14 +67,13 @@ bool ParseModJSON(const ScriptRequest& rq, const PIVFS& vfs, OsPath modsPath, Os
if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
return false;
if (!Script::ParseJSON(rq, modinfo.GetAsString(), json))
return false;
text = modinfo.GetAsString();
// Attempt to write the mod.json file so we'll take the fast path next time.
std::ofstream out_mod_json((modsPath / mod / L"mod.json").string8());
if (out_mod_json.good())
{
out_mod_json << modinfo.GetAsString();
out_mod_json << text;
out_mod_json.close();
}
else
@ -90,9 +90,33 @@ bool ParseModJSON(const ScriptRequest& rq, const PIVFS& vfs, OsPath modsPath, Os
{
std::stringstream buffer;
buffer << modjson.rdbuf();
return Script::ParseJSON(rq, buffer.str(), json);
text = buffer.str();
return true;
}
}
bool ParseModJSON(const ScriptRequest& rq, const PIVFS& vfs, OsPath modsPath, OsPath mod, Mod::ModData& data)
{
std::string text;
if (!LoadModJSON(vfs, modsPath, mod, text))
return false;
JS::RootedValue json(rq.cx);
if (!Script::ParseJSON(rq, text, &json))
return false;
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;
}
} // anonymous namespace
Mod& Mod::Instance()
@ -100,9 +124,9 @@ Mod& Mod::Instance()
return g_ModInstance;
}
JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) const
void Mod::UpdateAvailableMods(const ScriptInterface& scriptInterface)
{
PROFILE2("GetAvailableMods");
PROFILE2("UpdateAvailableMods");
const Paths paths(g_CmdLineArgs);
@ -120,15 +144,13 @@ JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) const
PIVFS vfs = CreateVfs();
ScriptRequest rq(scriptInterface);
JS::RootedValue value(rq.cx, Script::CreateObject(rq));
for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter)
{
JS::RootedValue json(rq.cx);
if (!ParseModJSON(rq, vfs, modPath, *iter, &json))
ModData data;
if (!ParseModJSON(rq, vfs, modPath, *iter, data))
continue;
// Valid mod data, add it to our structure
Script::SetProperty(rq, value, utf8_from_wstring(iter->string()).c_str(), json);
m_AvailableMods.emplace_back(std::move(data));
}
GetDirectoryEntries(modUserPath, NULL, &modDirsUser);
@ -139,14 +161,12 @@ JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) const
if (std::binary_search(modDirs.begin(), modDirs.end(), *iter))
continue;
JS::RootedValue json(rq.cx);
if (!ParseModJSON(rq, vfs, modUserPath, *iter, &json))
ModData data;
if (!ParseModJSON(rq, vfs, modUserPath, *iter, data))
continue;
// Valid mod data, add it to our structure
Script::SetProperty(rq, value, utf8_from_wstring(iter->string()).c_str(), json);
m_AvailableMods.emplace_back(std::move(data));
}
return value.get();
}
const std::vector<CStr>& Mod::GetEnabledMods() const
@ -179,21 +199,18 @@ bool Mod::EnableMods(const ScriptInterface& scriptInterface, const std::vector<C
if (counts["mod"] == 0)
m_EnabledMods.insert(m_EnabledMods.begin(), "mod");
ScriptRequest rq(scriptInterface);
JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface));
m_IncompatibleMods = CheckForIncompatibleMods(scriptInterface, m_EnabledMods, availableMods);
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));
CacheEnabledModVersions(scriptInterface);
return m_IncompatibleMods.empty();
}
std::vector<CStr> Mod::CheckForIncompatibleMods(const ScriptInterface& scriptInterface, const std::vector<CStr>& mods, const JS::RootedValue& availableMods) const
std::vector<CStr> Mod::CheckForIncompatibleMods(const std::vector<CStr>& mods) const
{
ScriptRequest rq(scriptInterface);
std::vector<CStr> incompatibleMods;
std::unordered_map<CStr, std::vector<CStr>> modDependencies;
std::unordered_map<CStr, CStr> modNameVersions;
@ -202,29 +219,17 @@ std::vector<CStr> Mod::CheckForIncompatibleMods(const ScriptInterface& scriptInt
if (mod == "mod")
continue;
JS::RootedValue modData(rq.cx);
std::vector<ModData>::const_iterator it = std::find_if(m_AvailableMods.begin(), m_AvailableMods.end(),
[&mod](const ModData& modData) { return modData.m_Pathname == mod; });
// Requested mod is not available, fail
if (!Script::HasProperty(rq, availableMods, mod.c_str()))
{
incompatibleMods.push_back(mod);
continue;
}
if (!Script::GetProperty(rq, availableMods, mod.c_str(), &modData))
if (it == m_AvailableMods.end())
{
incompatibleMods.push_back(mod);
continue;
}
std::vector<CStr> dependencies;
CStr version;
CStr name;
Script::GetProperty(rq, modData, "dependencies", dependencies);
Script::GetProperty(rq, modData, "version", version);
Script::GetProperty(rq, modData, "name", name);
modNameVersions.emplace(name, version);
modDependencies.emplace(mod, dependencies);
modNameVersions.emplace(it->m_Name, it->m_Version);
modDependencies.emplace(it->m_Name, it->m_Dependencies);
}
static const std::vector<CStr> toCheck = { "<=", ">=", "=", "<", ">" };
@ -308,35 +313,26 @@ bool Mod::CompareVersionStrings(const CStr& version, const CStr& op, const CStr&
return versionSize < requiredSize ? lt : gt;
}
void Mod::CacheEnabledModVersions(const ScriptInterface& scriptInterface)
{
ScriptRequest rq(scriptInterface);
JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface));
m_LoadedModVersions.clear();
for (const CStr& mod : m_EnabledMods)
{
// Ignore mod mod as it is irrelevant for compatibility checks
if (mod == "mod")
continue;
CStr version;
JS::RootedValue modData(rq.cx);
if (Script::GetProperty(rq, availableMods, mod.c_str(), &modData))
Script::GetProperty(rq, modData, "version", version);
m_LoadedModVersions.push_back({mod, version});
}
}
JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) const
{
std::vector<std::vector<CStr>> loadedMods;
for (const CStr& mod : m_EnabledMods)
{
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_Name, it->m_Version });
}
ScriptRequest rq(scriptInterface);
JS::RootedValue returnValue(rq.cx);
Script::ToJSVal(rq, &returnValue, m_LoadedModVersions);
Script::ToJSVal(rq, &returnValue, loadedMods);
return returnValue;
}
@ -357,3 +353,19 @@ JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface) const
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,7 +32,6 @@ public:
// Singleton-like interface.
static Mod& Instance();
JS::Value GetAvailableMods(const ScriptInterface& scriptInterface) const;
const std::vector<CStr>& GetEnabledMods() const;
const std::vector<CStr>& GetIncompatibleMods() const;
@ -61,26 +60,51 @@ public:
*/
JS::Value GetEngineInfo(const ScriptInterface& scriptInterface) const;
private:
/**
* This reads the version numbers from the launched mods.
* It caches the result, since the reading of zip files is slow and
* JS pages can request the version numbers too often easily.
* Make sure this is called after each MountMods call.
* Gets a dictionary of available mods and their complete, parsed mod.json data.
*/
void CacheEnabledModVersions(const ScriptInterface& scriptInterface);
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.
*/
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<CStr> 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<CStr> CheckForIncompatibleMods(const ScriptInterface& scriptInterface, const std::vector<CStr>& mods, const JS::RootedValue& availableMods) const;
std::vector<CStr> CheckForIncompatibleMods(const std::vector<CStr>& mods) const;
bool CompareVersionStrings(const CStr& required, const CStr& op, const CStr& version) const;
std::vector<CStr> m_EnabledMods;
// Of the currently loaded mods, these are the incompatible with the engine and cannot be loaded.
std::vector<CStr> m_IncompatibleMods;
std::vector<std::vector<CStr>> m_LoadedModVersions;
std::vector<ModData> m_AvailableMods;
};
#endif // INCLUDED_MOD

View File

@ -91,90 +91,43 @@ public:
ScriptRequest rq(script);
JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx));
CStr jsonString = "{\
\"name\": \"0ad\",\
\"version\" : \"0.0.25\",\
\"label\" : \"0 A.D. Empires Ascendant\",\
\"url\" : \"https://play0ad.com\",\
\"description\" : \"A free, open-source, historical RTS game.\",\
\"dependencies\" : []\
}\
";
JS::RootedValue json(rq.cx);
TS_ASSERT(Script::ParseJSON(rq, jsonString, &json));
JS_SetProperty(rq.cx, obj, "public", json);
JS::RootedValue jsonW(rq.cx);
CStr jsonStringW = "{\
\"name\": \"wrong\",\
\"version\" : \"0.0.25\",\
\"label\" : \"wrong mod\",\
\"url\" : \"\",\
\"description\" : \"fail\",\
\"dependencies\" : [\"0ad=0.0.24\"]\
}\
";
TS_ASSERT(Script::ParseJSON(rq, jsonStringW, &jsonW));
JS_SetProperty(rq.cx, obj, "wrong", jsonW);
JS::RootedValue jsonG(rq.cx);
CStr jsonStringG = "{\
\"name\": \"good\",\
\"version\" : \"0.0.25\",\
\"label\" : \"good mod\",\
\"url\" : \"\",\
\"description\" : \"ok\",\
\"dependencies\" : [\"0ad=0.0.25\"]\
}\
";
TS_ASSERT(Script::ParseJSON(rq, jsonStringG, &jsonG));
JS_SetProperty(rq.cx, obj, "good", jsonG);
JS::RootedValue jsonG2(rq.cx);
CStr jsonStringG2 = "{\
\"name\": \"good\",\
\"version\" : \"0.0.25\",\
\"label\" : \"good mod\",\
\"url\" : \"\",\
\"description\" : \"ok\",\
\"dependencies\" : [\"0ad>=0.0.24\"]\
}\
";
TS_ASSERT(Script::ParseJSON(rq, jsonStringG2, &jsonG2));
JS_SetProperty(rq.cx, obj, "good2", jsonG2);
JS::RootedValue availableMods(rq.cx, JS::ObjectValue(*obj));
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" }, "" },
};
std::vector<CStr> mods;
mods.clear();
mods.push_back("public");
TS_ASSERT(m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(m_Mods.CheckForIncompatibleMods(mods).empty());
mods.clear();
mods.push_back("mod");
mods.push_back("public");
TS_ASSERT(m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(m_Mods.CheckForIncompatibleMods(mods).empty());
mods.clear();
mods.push_back("public");
mods.push_back("good");
TS_ASSERT(m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(m_Mods.CheckForIncompatibleMods(mods).empty());
mods.clear();
mods.push_back("public");
mods.push_back("good2");
TS_ASSERT(m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(m_Mods.CheckForIncompatibleMods(mods).empty());
mods.clear();
mods.push_back("public");
mods.push_back("wrong");
TS_ASSERT(!m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(!m_Mods.CheckForIncompatibleMods(mods).empty());
mods.clear();
mods.push_back("public");
mods.push_back("does_not_exist");
TS_ASSERT(!m_Mods.CheckForIncompatibleMods(script, mods, availableMods).empty());
TS_ASSERT(!m_Mods.CheckForIncompatibleMods(mods).empty());
}
};