Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -396,6 +396,11 @@
[mod]
enabledmods = "mod public"
+[modio.v1]
+baseurl = "https://api.mod.io/v1"
+api_key = "acf8fc07e3a8e9228ef4d5704c1659a1"
+name_id = "0ad"
+
[network]
duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join.
lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
Index: binaries/data/mods/mod/gui/modmod/modio.js
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/modmod/modio.js
@@ -0,0 +1,65 @@
+let g_Mods = [];
+
+function init()
+{
+ g_Mods = Engine.ModIoGetMods();
+
+ generateModsList(g_Mods);
+}
+
+function generateModsList(mods)
+{
+ var [keys, names, name_ids, versions, filenames, filesizes, filehash_md5s, downloads] = [[],[],[],[],[],[],[],[]];
+
+ let i = 0;
+ for (let mod of mods)
+ {
+ keys.push(i++);
+ names.push(mod.name);
+ name_ids.push(mod.name_id);
+ versions.push(mod.version);
+ filenames.push(mod.filename);
+ filesizes.push(mod.filesize);
+ filehash_md5s.push(mod.filehash_md5);
+ downloads.push(mod.download);
+ }
+
+ var obj = Engine.GetGUIObjectByName("modsAvailableList");
+ obj.list_name = names;
+ obj.list_modVersion = versions;
+ obj.list_modname_id = name_ids;
+ obj.list_modfilename = filenames;
+ obj.list_modfilesize = filesizes;
+ obj.list_modfilehash_md5 = filehash_md5s;
+ obj.list_moddownload = downloads;
+
+ obj.list = keys;
+}
+
+function showModDescription()
+{
+ let listObject = Engine.GetGUIObjectByName("modsAvailableList");
+ let desc = "No mod has been selected.";
+ if (listObject.selected != -1)
+ desc = g_Mods[listObject.selected].summary;
+
+ Engine.GetGUIObjectByName("globalModDescription").caption = desc;
+}
+
+function downloadMod()
+{
+ let listObject = Engine.GetGUIObjectByName("modsAvailableList");
+ if (listObject.selected == -1)
+ {
+ warn("Select something first.");
+ return;
+ }
+
+ Engine.ModIoDownloadMod(listObject.selected); // TODO in case we support sorting or filtering this should be the keys value.
+ // TODO: pass index to engine to download that. We cannot be trusted with anything but an index (and even that just barely).
+}
+
+function closePage()
+{
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+}
Index: binaries/data/mods/mod/gui/modmod/modio.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/modmod/modio.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
Index: binaries/data/mods/mod/gui/modmod/modmod.xml
===================================================================
--- binaries/data/mods/mod/gui/modmod/modmod.xml
+++ binaries/data/mods/mod/gui/modmod/modmod.xml
@@ -186,8 +186,8 @@
- Cancel
- closePage();
+ mod.io test
+ Engine.SwitchGuiPage("page_modio.xml");
Index: binaries/data/mods/mod/gui/page_modio.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/page_modio.xml
@@ -0,0 +1,9 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ modmod/styles.xml
+ modmod/modio.xml
+
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -68,6 +68,7 @@
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
+#include "ps/ModIo.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
@@ -734,6 +735,8 @@
SAFE_DELETE(g_XmppClient);
+ SAFE_DELETE(g_ModIo);
+
ShutdownPs();
TIMER_BEGIN(L"shutdown TexMan");
Index: source/ps/ModIo.h
===================================================================
--- /dev/null
+++ source/ps/ModIo.h
@@ -0,0 +1,65 @@
+/* Copyright (C) 2017 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_MODIO
+#define INCLUDED_MODIO
+
+#include
+
+#include "lib/external_libraries/curl.h"
+
+#include "scriptinterface/ScriptInterface.h"
+
+class ModIo
+{
+ NONCOPYABLE(ModIo);
+public:
+ ModIo();
+ ~ModIo();
+
+ const std::vector>& GetMods(const ScriptInterface& scriptinterface);
+
+ void DownloadMod(size_t idx);
+
+private:
+ static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp);
+
+ bool ParseGameIdResponse(const ScriptInterface& scriptInterface);
+ bool ParseModsResponse(const ScriptInterface& scriptInterface);
+
+ void PrintMods();
+
+ // Url parts
+ std::string m_BaseUrl;
+ std::string m_GamesRequest;
+ std::string m_GameId;
+
+ // Query parameters
+ std::string m_ApiKey;
+ std::string m_IdQuery;
+
+ CURL* m_Curl;
+ curl_slist* m_Headers;
+ char m_ErrorBuffer[CURL_ERROR_SIZE];
+ std::string m_ResponseData;
+
+ std::vector> m_ModData;
+};
+
+extern ModIo* g_ModIo;
+
+#endif // INCLUDED_MODIO
Index: source/ps/ModIo.cpp
===================================================================
--- /dev/null
+++ source/ps/ModIo.cpp
@@ -0,0 +1,310 @@
+/* Copyright (C) 2017 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+// TODO: This looks like it might be useful to others, MIT?
+
+// TODO: Comment on why we handle things this way (not relying on user input, not trusting the remote too much, etc.)
+
+// TODO: Better error messages. Also ask upstream about some special queries with partially malformed replies
+
+#include "precompiled.h"
+
+#include "ModIo.h"
+
+#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
+#include "scriptinterface/ScriptConversions.h"
+
+ModIo* g_ModIo = nullptr;
+
+ModIo::ModIo()
+ : m_GamesRequest("/games")
+{
+ // Get config values from the sytem namespace, or below (default); this can be overridden on the command line.
+ // We do this so a malicious mod cannot change the base url and get the user to make connections
+ // to someone else's endpoint.
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.baseurl", m_BaseUrl);
+ // TODO: Should we allow mods to actually change the two settings below? (Might be nice for total conversions.)
+ {
+ std::string api_key;
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.api_key", api_key);
+ m_ApiKey = "api_key=" + api_key;
+ }
+ {
+ std::string nameid;
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.name_id", nameid);
+ m_IdQuery = "name_id="+nameid;
+ }
+
+ // Initialise everything except Win32 sockets (because our networking
+ // system already inits those)
+ curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
+// TODO we should only do this in one single place (same for shutdown, currently we do it here and in the userreporter)
+
+ m_Curl = curl_easy_init();
+ ENSURE(m_Curl);
+
+ // TODO: Do we actually care?
+ // Capture error messages
+ curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
+
+ // Disable signal handlers (required for multithreaded applications)
+ curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
+
+ // To minimise security risks, don't support redirects
+ curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
+
+ // Set IO callbacks
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
+
+ m_Headers = NULL;
+ std::string ua = "User-Agent: pyrogenesis ";
+ ua += curl_version();
+ ua += " (https://play0ad.com/)";
+ m_Headers = curl_slist_append(m_Headers, ua.c_str());
+ // TODO more?
+ curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
+
+
+ // we only accept indices from JS when downloading something later on (security)
+}
+
+ModIo::~ModIo()
+{
+ curl_slist_free_all(m_Headers);
+ curl_easy_cleanup(m_Curl);
+ curl_global_cleanup(); // TODO
+}
+
+size_t ModIo::ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
+{
+ ModIo* self = static_cast(userp);
+
+ self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
+
+ return size*nmemb;
+}
+
+const std::vector>& ModIo::GetMods(const ScriptInterface& scriptInterface)
+{
+ m_ModData.clear();
+
+ // Get the game id if we didn't fetch it already
+ if (m_GameId.empty())
+ {
+ std::string url = m_BaseUrl+m_GamesRequest+"?"+m_ApiKey+"&"+m_IdQuery;
+ curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
+ CURLcode err = curl_easy_perform(m_Curl);
+
+ if (err != CURLE_OK)
+ {
+ LOGERROR("Server said no: %d", err);
+ return m_ModData;
+ }
+
+ if (!ParseGameIdResponse(scriptInterface))
+ return m_ModData;
+ }
+
+ // Get the actual mods
+ std::string url = m_BaseUrl+m_GamesRequest+m_GameId+"/mods?"+m_ApiKey;
+ curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
+ CURLcode err = curl_easy_perform(m_Curl);
+ if (err != CURLE_OK)
+ {
+ LOGERROR("Server said no to mods %d", err);
+ return m_ModData;
+ }
+
+ if (!ParseModsResponse(scriptInterface))
+ m_ModData.clear(); // Failed during parsing, make sure we don't provide partial data
+
+ PrintMods();
+
+ return m_ModData;
+}
+
+#define FAIL(...) STMT(LOGERROR(__VA_ARGS__); return false;)
+
+/**
+ * Parses the current content of m_ResponseData to extract m_GameId.
+ *
+ * The JSON data is expected to look like
+ * { "data": [{"id": 42, ...}, ...], ... }
+ * where we are only interested in the value of the id property.
+ *
+ * @returns true iff it successfully parsed the id.
+ */
+bool ModIo::ParseGameIdResponse(const ScriptInterface& scriptInterface)
+{
+ JSContext* cx = scriptInterface.GetContext();
+ JS::RootedValue gameResponse(cx);
+
+ // What the data is expected to look like, and what we need.
+ // There is also a result_count property at the same level as data, which should always be 1 following
+ // our query, but maybe we should check that (nah)
+ if (!scriptInterface.ParseJSON(m_ResponseData, &gameResponse))
+ FAIL("Failed to parse response as JSON.");
+
+ m_ResponseData.clear();
+
+ JS::RootedObject gameResponseObj(cx, gameResponse.toObjectOrNull());
+ // TODO handle it being null, or does JS_GetProperty do that for us?
+ JS::RootedValue dataVal(cx);
+ if (!JS_GetProperty(cx, gameResponseObj, "data", &dataVal))
+ FAIL("data property not in response.");
+
+ // [{"id": 42, ...}, ...]
+ if (!dataVal.isObject())
+ FAIL("data property not an object");
+
+ JS::RootedObject data(cx);
+ data = &dataVal.toObject();
+ u32 length;
+ if (!JS_IsArrayObject(cx, data) || !JS_GetArrayLength(cx, data, &length) || !length)
+ FAIL("data property not an array with at least one element");
+
+ // {"id": 42, ...}
+ JS::RootedValue first(cx);
+ if (!JS_GetElement(cx, data, 0, &first))
+ FAIL("couldn't get first element.");
+
+ int id = -1;
+ if (!ScriptInterface::FromJSProperty(cx, first, "id", id))
+ FAIL("couldn't get id");
+
+ m_GameId = "/" + std::to_string(id);
+ return true;
+}
+
+/**
+ * Parses the current content of m_ResponseData into m_ModData.
+ *
+ * The JSON data is expected to look like
+ * { data: [modobj1, modobj2, ...], ... (including result_count) }
+ * where modobjN has the following structure
+ * { homepage: "url", name: "displayname", nameid: "short-non-whitespace-name",
+ * summary: "short desc.", modfile: { version: "1.2.4", filename: "asdf.zip",
+ * filehash: "md5sum", filesize: 1234, download: "someurl" }, ... }.
+ * Only the listed properties are of interest to consumers, and we flatten
+ * the modfile structure as that simplifies handling and there are no conflicts.
+ */
+bool ModIo::ParseModsResponse(const ScriptInterface& scriptInterface)
+{
+ JSContext* cx = scriptInterface.GetContext();
+ JS::RootedValue modResponse(cx);
+
+ if (!scriptInterface.ParseJSON(m_ResponseData, &modResponse))
+ FAIL("Failed to parse response as JSON.");
+
+ m_ResponseData.clear();
+
+ JS::RootedObject modResponseObj(cx, modResponse.toObjectOrNull());
+ // TODO handle it being null, or does JS_GetProperty do that for us?
+ JS::RootedValue dataVal(cx);
+ if (!JS_GetProperty(cx, modResponseObj, "data", &dataVal))
+ FAIL("data property not in response.");
+
+ // [modobj1, modobj2, ... ]
+ if (!dataVal.isObject())
+ FAIL("data property not an object");
+
+ JS::RootedObject data(cx);
+ data = &dataVal.toObject();
+ u32 length;
+ if (!JS_IsArrayObject(cx, data) || !JS_GetArrayLength(cx, data, &length) || !length)
+ FAIL("data property not an array with at least one element");
+
+ m_ModData.clear();
+ m_ModData.reserve(length);
+
+ for (u32 i = 0; i < length; ++i)
+ {
+ JS::RootedValue el(cx);
+ if (!JS_GetElement(cx, data, i, &el) || !el.isObject())
+ FAIL("Failed to get array element object");
+
+ m_ModData.emplace_back();
+
+#define COPY_STRINGS(prefix, obj, ...) \
+ for (const std::string& prop : { __VA_ARGS__ }) \
+ { \
+ std::string val; \
+ ScriptInterface::FromJSProperty(cx, obj, prop.c_str(), val); \
+ m_ModData.back().emplace(prefix+prop, val); \
+ }
+
+ // TODO: Currently the homepage field does not contain a non-null value for any entry.
+ // TODO: look into storing the remaining mod.json things we need in metatdata_blob (deps
+ COPY_STRINGS("", el, "name", "name_id", "summary");
+
+ // Now copy over the modfile part, but without the pointless substructure
+ JS::RootedObject elObj(cx);
+ elObj = &el.toObject();
+ JS::RootedValue modFile(cx);
+ if (!JS_GetProperty(cx, elObj, "modfile", &modFile))
+ FAIL("Failed to get modfile data");
+
+ COPY_STRINGS("", modFile, "version", "filename", "filesize", "download");
+
+ JS::RootedObject modFileObj(cx);
+ modFileObj = &modFile.toObject();
+ JS::RootedValue filehash(cx);
+ if (!JS_GetProperty(cx, modFileObj, "filehash", &filehash))
+ FAIL("Failed to get filehash data");
+
+ // TODO: Actually copy all elements of this over? (So we get them automatically instead of having to do this manually)
+ COPY_STRINGS("filehash_", filehash, "md5");
+
+#undef COPY_STRINGS
+ }
+
+ return true;
+}
+
+#undef FAIL
+
+void ModIo::DownloadMod(size_t idx)
+{
+ if (idx >= m_ModData.size())
+ return;
+
+ LOGERROR("TODO: Download mod %d", idx);
+ printf("{\n");
+ for (const auto& b : m_ModData[idx])
+ {
+ printf(" \"%s\": \"%s\",\n", b.first.c_str(), b.second.c_str());
+ }
+ printf("}\n");
+}
+
+// TODO: nuke this at some point
+void ModIo::PrintMods()
+{
+ printf("[\n");
+ for (const auto& a : m_ModData)
+ {
+ printf(" {\n");
+ for (const auto& b : a)
+ {
+ printf(" \"%s\": \"%s\",\n", b.first.c_str(), b.second.c_str());
+ }
+ printf(" },\n");
+ }
+ printf("]\n");
+}
Index: source/ps/scripting/JSInterface_Mod.h
===================================================================
--- source/ps/scripting/JSInterface_Mod.h
+++ source/ps/scripting/JSInterface_Mod.h
@@ -26,6 +26,9 @@
JS::Value GetAvailableMods(ScriptInterface::CxPrivate* pCxPrivate);
void RestartEngine(ScriptInterface::CxPrivate* pCxPrivate);
void SetMods(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& mods);
+
+ JS::Value ModIoGetMods(ScriptInterface::CxPrivate* pCxPrivate);
+ void ModIoDownloadMod(ScriptInterface::CxPrivate* pCxPrivate, uint32_t idx);
}
#endif
Index: source/ps/scripting/JSInterface_Mod.cpp
===================================================================
--- source/ps/scripting/JSInterface_Mod.cpp
+++ source/ps/scripting/JSInterface_Mod.cpp
@@ -27,6 +27,7 @@
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Mod.h"
+#include "ps/ModIo.h"
#include "ps/Profile.h"
#include "ps/scripting/JSInterface_Mod.h"
@@ -123,9 +124,49 @@
g_modsLoaded = mods;
}
+JS::Value JSI_Mod::ModIoGetMods(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+
+ if (!g_ModIo)
+ g_ModIo = new ModIo();
+
+ JS::RootedValue mods(cx);
+
+ if (!g_ModIo)
+ return mods; // TODO: error/warning?
+
+ scriptInterface->Eval("([])", &mods);
+ for (const std::map& mod : g_ModIo->GetMods(*scriptInterface))
+ {
+ JS::RootedValue m(cx);
+ scriptInterface->Eval("({})", &m);
+ for (const std::pair& prop : mod)
+ scriptInterface->SetProperty(m, prop.first.c_str(), prop.second, true);
+
+ scriptInterface->CallFunctionVoid(mods, "push", m);
+ }
+
+ return mods;
+}
+
+void JSI_Mod::ModIoDownloadMod(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), uint32_t idx)
+{
+ if (!g_ModIo)
+ return; // TODO: Warn and tell users to fix their code?
+
+ g_ModIo->DownloadMod(idx);
+}
+
void JSI_Mod::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetAvailableMods");
scriptInterface.RegisterFunction("RestartEngine");
scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods");
+
+ scriptInterface.RegisterFunction("ModIoGetMods");
+ scriptInterface.RegisterFunction("ModIoDownloadMod");
}