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:
parent
b56f0222d9
commit
498f0d420b
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user