diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg
index 89465ff673..20f477c322 100644
--- a/binaries/data/config/default.cfg
+++ b/binaries/data/config/default.cfg
@@ -374,3 +374,6 @@ lobby.history = 0 ; Number of past messages to display o
; Overlay Preferences
overlay.fps = "false" ; Show frames per second in top right corner
overlay.realtime = "false" ; Show current system time in top right corner
+
+; MOD SETTINGS
+;mod.enabledmods = "mod public"
diff --git a/binaries/data/mods/mod/gui/modmod/modmod.js b/binaries/data/mods/mod/gui/modmod/modmod.js
new file mode 100644
index 0000000000..712dca61ca
--- /dev/null
+++ b/binaries/data/mods/mod/gui/modmod/modmod.js
@@ -0,0 +1,494 @@
+/*
+Example contents of g_mods:
+{
+ "foldername1": { // this is the content of the json file in a specific mod
+ name: "unique_shortname", // eg "0ad", "rote"
+ version: "0.0.16",
+ label: "Nice Mod Name", // eg "0 A.D. - Empires Ascendant"
+ type: "content|functionality|mixed/mod-pack",
+ url: "http://wildfregames.com/",
+ description: "",
+ dependencies: [] // (name({<,<=,==,>=,>}version)?)+
+ },
+ "foldername2": {
+ name: "mod2",
+ label: "Mod 2",
+ version: "1.1",
+ type: "content|functionality|mixed/mod-pack",
+ url: "http://play0ad.wfg.com/",
+ description: "",
+ dependencies: []
+ }
+}
+*/
+
+
+var g_mods = {}; // Contains all JSONs as explained in the structure above
+var g_modsEnabled = []; // folder names
+var g_modsAvailable = []; // folder names
+
+const g_sortByOptions = [translate("Name"), translate("Label"), translate("Folder"), translate("Version")];
+const SORT_BY_NAME = 0;
+const SORT_BY_FOLDER = 1;
+const SORT_BY_LABEL = 2;
+const SORT_BY_VERSION = 3;
+
+var g_modTypes = [translate("Type: Any")];
+
+/**
+ * Fetches the mod lists in JSON from the Engine.
+ * Initiates a first creation of the GUI lists.
+ * Enabled mods are read from the Configuration and checked if still available.
+ */
+function init()
+{
+ g_mods = Engine.GetAvailableMods();
+
+ g_modsEnabled = getExistingModsFromConfig();
+ g_modsAvailable = Object.keys(g_mods).filter(function(i) { return g_modsEnabled.indexOf(i) === -1; });
+
+ Engine.GetGUIObjectByName("negateFilter").checked = false;
+ Engine.GetGUIObjectByName("modGenericFilter").caption = translate("Filter");
+ Engine.GetGUIObjectByName("modTypeFilter").selected = 0;
+
+ var sortBy = Engine.GetGUIObjectByName("sortBy");
+ sortBy.list = g_sortByOptions;
+ sortBy.selected = SORT_BY_NAME;
+
+ // sort ascending by default
+ Engine.GetGUIObjectByName("isOrderDescending").checked = false;
+
+ generateModsLists();
+
+ Engine.GetGUIObjectByName("message").caption = translate("Message: Mods Loaded.");
+}
+
+/**
+ * Recreating both the available and enabled mods lists.
+ */
+function generateModsLists()
+{
+ generateModsList('modsAvailableList', g_modsAvailable);
+ generateModsList('modsEnabledList', g_modsEnabled);
+}
+
+function saveMods()
+{
+ // always sort mods before saving
+ sortMods();
+ Engine.ConfigDB_CreateValue("user", "mod.enabledmods", ["mod"].concat(g_modsEnabled).join(" "));
+ Engine.ConfigDB_WriteFile("user", "config/user.cfg");
+}
+
+function startMods()
+{
+ // always sort mods before starting
+ sortMods();
+ Engine.SetMods(["mod"].concat(g_modsEnabled));
+ Engine.RestartEngine();
+}
+
+function getExistingModsFromConfig()
+{
+ var existingMods = [];
+
+ var mods = [];
+ var cfgMods = Engine.ConfigDB_GetValue("user", "mod.enabledmods");
+ if (cfgMods.length > 0)
+ mods = cfgMods.split(/\s+/);
+
+ mods.forEach(function(mod) {
+ if (mod in g_mods)
+ existingMods.push(mod);
+ });
+
+ return existingMods;
+}
+
+/**
+ * (Re-)Generate List of all mods.
+ * @param listObjectName The GUI object's name (e.g. "modsEnabledList", "modsAvailableList")
+ */
+function generateModsList(listObjectName, mods)
+{
+ var sortBy = Engine.GetGUIObjectByName("sortBy");
+ var orderDescending = Engine.GetGUIObjectByName("isOrderDescending");
+ var isDescending = orderDescending && orderDescending.checked;
+
+ // TODO: Sorting mods by dependencies would be nice
+ if (listObjectName != "modsEnabledList")
+ {
+ var idx = -1;
+ if (sortBy)
+ idx = sortBy.selected;
+
+ switch (idx)
+ {
+ default:
+ warn("generateModsList: invalid index '"+idx+"'"); // fall through
+ // sort by unique name alphanumerically by default:
+ case -1:
+ case SORT_BY_NAME:
+ mods.sort(function(a, b)
+ {
+ var ret = compare(g_mods[a].name.toLowerCase(), g_mods[b].name.toLowerCase());
+ return ret * (isDescending ? -1 : 1);
+ });
+ break;
+ case SORT_BY_FOLDER:
+ mods.sort(function(a, b)
+ {
+ return compare(a.toLowerCase(), b.toLowerCase()) * (isDescending ? -1 : 1);
+ });
+ break;
+ case SORT_BY_LABEL:
+ mods.sort(function(a, b)
+ {
+ var ret = compare(g_mods[a].label.toLowerCase(), g_mods[b].label.toLowerCase());
+ return ret * (isDescending ? -1 : 1);
+ });
+ break;
+ case SORT_BY_VERSION:
+ mods.sort(function(a, b)
+ {
+ // TODO reuse actual logic
+ var ret = compare(g_mods[a].version, g_mods[b].version);
+ return ret * (isDescending ? -1 : 1);
+ });
+ break;
+ }
+ }
+
+ var [names, folders, labels, types, urls, versions, dependencies] = [[],[],[],[],[],[],[]];
+ mods.forEach(function(foldername)
+ {
+ var mod = g_mods[foldername];
+ if (mod.type && g_modTypes.indexOf(mod.type) == -1)
+ g_modTypes.push(mod.type);
+
+ if (filterMod(foldername))
+ return;
+
+ names.push(mod.name);
+ folders.push('[color="45 45 45"](' + foldername + ')[/color]');
+
+ labels.push(mod.label || "");
+ types.push(mod.type || "");
+ urls.push(mod.url || "");
+ versions.push(mod.version || "");
+ dependencies.push((mod.dependencies || []).join(" "));
+ });
+
+ // Update the list
+ var obj = Engine.GetGUIObjectByName(listObjectName);
+ obj.list_name = names;
+ obj.list_modFolderName = folders;
+ obj.list_modLabel = labels;
+ obj.list_modType = types;
+ obj.list_modURL = urls;
+ obj.list_modVersion = versions;
+ obj.list_modDependencies = dependencies;
+
+ obj.list = names;
+
+ var modTypeFilter = Engine.GetGUIObjectByName("modTypeFilter");
+ modTypeFilter.list = g_modTypes;
+}
+
+function compare(a, b)
+{
+ return ( (a > b) ? 1 : (b > a) ? -1 : 0 );
+}
+
+function enableMod()
+{
+ var obj = Engine.GetGUIObjectByName("modsAvailableList");
+ var pos = obj.selected;
+ if (pos === -1)
+ return;
+
+ var mod = g_modsAvailable[pos];
+
+ // Move it to the other table
+ // check dependencies, warn about not satisfied dependencies and abort if so:
+ if (!areDependenciesMet(mod))
+ return;
+
+ g_modsEnabled.push(g_modsAvailable.splice(pos, 1)[0]);
+
+ if (pos >= g_modsAvailable.length)
+ pos--;
+ obj.selected = pos;
+
+ generateModsLists();
+}
+
+function disableMod()
+{
+ var obj = Engine.GetGUIObjectByName("modsEnabledList");
+ var pos = obj.selected;
+ if (pos === -1)
+ return;
+
+ var mod = g_modsEnabled[pos];
+
+ g_modsAvailable.push(g_modsEnabled.splice(pos, 1)[0]);
+
+ // Remove mods that required the removed mod and cascade
+ // Sort them, so we know which ones can depend on the removed mod
+ // TODO: Find position where the removed mod would have fit (for now assume idx 0)
+ sortMods();
+ for (var i = 0; i < g_modsEnabled.length; ++i)
+ {
+ if (!areDependenciesMet(g_modsEnabled[i]))
+ {
+ g_modsAvailable.push(g_modsEnabled.splice(i, 1)[0]);
+ --i;
+ }
+ }
+
+ // select the last element even if more than 1 mod has been removed:
+ if (pos > g_modsEnabled.length - 1)
+ pos = g_modsEnabled.length - 1;
+ obj.selected = pos;
+
+ generateModsLists();
+}
+
+function resetFilters()
+{
+ // Reset states of gui objects.
+ Engine.GetGUIObjectByName("modTypeFilter").selected = 0;
+ Engine.GetGUIObjectByName("negateFilter").checked = false;
+ Engine.GetGUIObjectByName("modGenericFilter").caption = "";
+
+ // NOTE: Calling generateModsLists() is not needed as the selection changes and that calls applyFilters()
+}
+
+function applyFilters()
+{
+ Engine.GetGUIObjectByName("modsAvailableList").selected = -1;
+ Engine.GetGUIObjectByName("modsEnabledList").selected = -1;
+ generateModsLists();
+}
+
+/**
+ * Filter a mod based on the status of the filters.
+ *
+ * @param modFolder Mod to be tested.
+ * @return True if mod should not be displayed.
+ */
+function filterMod(modFolder)
+{
+ var mod = g_mods[modFolder];
+
+ var modTypeFilter = Engine.GetGUIObjectByName("modTypeFilter");
+ var genericFilter = Engine.GetGUIObjectByName("modGenericFilter");
+ var negateFilter = Engine.GetGUIObjectByName("negateFilter");
+
+ // TODO: and result of filters together (type && generic)
+
+ // We assume index 0 means display all for any given filter.
+ if (modTypeFilter.selected > 0
+ && (mod.type || "") != modTypeFilter.list[modTypeFilter.selected])
+ return !negateFilter.checked;
+
+ if (genericFilter && genericFilter.caption && genericFilter.caption != "" && genericFilter.caption != "Filter")
+ {
+ var t = genericFilter.caption;
+ if (modFolder.indexOf(t) === -1
+ && mod.name.indexOf(t) === -1
+ && mod.label.indexOf(t) === -1
+ && (mod.type || "").indexOf(t) === -1
+ && mod.url.indexOf(t) === -1
+ && mod.version.indexOf(t) === -1
+ && mod.description.indexOf(t) === -1
+ && mod.dependencies.indexOf(t) === -1)
+ {
+ return !negateFilter.checked;
+ }
+ }
+
+ return negateFilter.checked;
+}
+
+function closePage()
+{
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+}
+
+
+/**
+ * Moves an item in the list @p objectName up or down depending on the value of @p up.
+ */
+function moveCurrItem(objectName, up)
+{
+ // reuse the check for null and if something is selected.
+ if (getCurrItemValue(objectName) == "")
+ return;
+
+ var idx = Engine.GetGUIObjectByName(objectName).selected;
+ if (idx === -1)
+ return;
+
+ var num = getNumItems(objectName);
+ var idx2 = idx + (up ? -1 : 1);
+ if (idx2 < 0 || idx2 >= num)
+ return;
+
+ var tmp = g_modsEnabled[idx];
+ g_modsEnabled[idx] = g_modsEnabled[idx2];
+ g_modsEnabled[idx2] = tmp;
+
+ // Selected object reached the new position.
+ Engine.GetGUIObjectByName(objectName).list = g_modsEnabled;
+ Engine.GetGUIObjectByName(objectName).selected = idx2;
+ generateModsList('modsEnabledList', g_modsEnabled);
+}
+
+function areDependenciesMet(mod)
+{
+ var guiObject = Engine.GetGUIObjectByName("message");
+ for each (var dependency in g_mods[mod].dependencies)
+ {
+ if (isDependencyMet(dependency))
+ continue;
+ guiObject.caption = '[color="250 100 100"]' + translate(sprintf('Dependency not met: %(dep)s', { "dep": dependency })) +'[/color]';
+ return false;
+ }
+
+ guiObject.caption = '[color="100 250 100"]' + translate('All dependencies met') + '[/color]';
+
+ return true;
+}
+
+/**
+ * @param dependency: Either id (unique modJson.name) and version or only the unique mod name.
+ * Concatenated by either "=", ">", "<", ">=", "<=".
+ */
+function isDependencyMet(dependency_idAndVersion, modsEnabled = null)
+{
+ if (!modsEnabled)
+ modsEnabled = g_modsEnabled;
+
+ // Split on {=,<,<=,>,>=} and use the second part as the version number
+ // and whatever we split on as a way to handle that version.
+ var op = dependency_idAndVersion.match(/(<=|>=|<|>|=)/);
+ // Did the dependency contain a version number?
+ if (op)
+ {
+ op = op[0];
+ var dependency_parts = dependency_idAndVersion.split(op);
+ var dependency_version = dependency_parts[1];
+ var dependency_id = dependency_parts[0];
+ }
+ else
+ var dependency_id = dependency_idAndVersion;
+
+ // modsEnabled_key currently is the mod folder name.
+ for each (var modsEnabled_key in modsEnabled)
+ {
+ var modJson = g_mods[modsEnabled_key];
+ if (modJson.name != dependency_id)
+ continue;
+
+ // There could be another mod with a satisfying version
+ if (!op || versionSatisfied(modJson.version, op, dependency_version))
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Returns true if @p version satisfies @p op (<,<=,=,>=,>) @p requirement.
+ * @note @p version and @p requirement are split on '.' and everything after
+ * '-' or '_' is ignored. Only numbers are supported.
+ * @note "5.3" < "5.3.0"
+ */
+function versionSatisfied(version, op, requirement)
+{
+ var reqList = requirement.split(/[-_]/)[0].split(/\./g);
+ var avList = version.split(/[-_]/)[0].split(/\./g);
+
+ var eq = op.indexOf("=") !== -1;
+ var lt = op.indexOf("<") !== -1;
+ var gt = op.indexOf(">") !== -1;
+ if (!(eq || lt || gt))
+ {
+ warn("No valid compare op");
+ return false;
+ }
+
+ var l = Math.min(reqList.length, avList.length);
+ for (var i = 0; i < l; ++i)
+ {
+ // TODO: Handle NaN
+ var diff = +avList[i] - +reqList[i];
+
+ // Early success
+ if (gt && diff > 0)
+ return true;
+ if (lt && diff < 0)
+ return true;
+
+ // Early failure
+ if (gt && diff < 0)
+ return false;
+ if (lt && diff > 0)
+ return false;
+ if (eq && diff !== 0)
+ return false;
+ }
+ // common prefix matches
+ var ldiff = avList.length - reqList.length;
+ if (ldiff === 0)
+ return eq;
+ // NB: 2.3 != 2.3.0
+ if (ldiff < 0)
+ return lt;
+ if (ldiff > 0)
+ return gt;
+
+ // Can't be reached
+ error("version checking code broken");
+ return false;
+}
+
+function sortMods()
+{
+ // store the list of dependencies per mod, but strip the version numbers
+ var deps = {};
+ for (var mod of g_modsEnabled)
+ {
+ deps[mod] = [];
+ if (!g_mods[mod].dependencies)
+ continue;
+ deps[mod] = g_mods[mod].dependencies.map(function(d) { return d.split(/(<=|>=|<|>|=)/)[0]; });
+ }
+ var sortFunction = function(mod1, mod2)
+ {
+ var name1 = g_mods[mod1].name;
+ var name2 = g_mods[mod2].name;
+ if (deps[mod1].indexOf(name2) != -1)
+ return 1;
+ if (deps[mod2].indexOf(name1) != -1)
+ return -1;
+ return 0;
+ }
+ g_modsEnabled.sort(sortFunction);
+ generateModsList("modsEnabledList", g_modsEnabled);
+}
+
+function showModDescription(listObjectName, mod_keys)
+{
+ var listObject = Engine.GetGUIObjectByName(listObjectName);
+ if (listObject.selected == -1)
+ var desc = '[color="255 100 100"]' + translate("No mod has been selected.") + '[/color]';
+ else
+ {
+ var mod_key = mod_keys[listObject.selected];
+ var desc = g_mods[mod_key].description;
+ }
+
+ Engine.GetGUIObjectByName("globalModDescription").caption = desc;
+}
diff --git a/binaries/data/mods/mod/gui/modmod/modmod.xml b/binaries/data/mods/mod/gui/modmod/modmod.xml
new file mode 100644
index 0000000000..53a3cdfa4d
--- /dev/null
+++ b/binaries/data/mods/mod/gui/modmod/modmod.xml
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/mod/gui/modmod/styles.xml b/binaries/data/mods/mod/gui/modmod/styles.xml
new file mode 100644
index 0000000000..ade99164d9
--- /dev/null
+++ b/binaries/data/mods/mod/gui/modmod/styles.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/binaries/data/mods/mod/gui/page_modmod.xml b/binaries/data/mods/mod/gui/page_modmod.xml
new file mode 100644
index 0000000000..06aa9f58b4
--- /dev/null
+++ b/binaries/data/mods/mod/gui/page_modmod.xml
@@ -0,0 +1,21 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprite1.xml
+ common/styles.xml
+ common/common_sprites.xml
+ common/common_styles.xml
+ common/init.xml
+
+ pregame/sprites.xml
+ pregame/styles.xml
+
+ modmod/styles.xml
+ modmod/modmod.xml
+
+ common/global.xml
+
diff --git a/binaries/data/mods/mod/gui/page_pregame.xml b/binaries/data/mods/mod/gui/page_pregame.xml
new file mode 100644
index 0000000000..7c09b6b7a8
--- /dev/null
+++ b/binaries/data/mods/mod/gui/page_pregame.xml
@@ -0,0 +1,4 @@
+
+
+ pregame/mainmenu.xml
+
diff --git a/binaries/data/mods/mod/gui/pregame/mainmenu.js b/binaries/data/mods/mod/gui/pregame/mainmenu.js
new file mode 100644
index 0000000000..7cddb09c6a
--- /dev/null
+++ b/binaries/data/mods/mod/gui/pregame/mainmenu.js
@@ -0,0 +1,4 @@
+function init()
+{
+ Engine.SwitchGuiPage("page_modmod.xml", {});
+}
diff --git a/binaries/data/mods/mod/gui/pregame/mainmenu.xml b/binaries/data/mods/mod/gui/pregame/mainmenu.xml
new file mode 100644
index 0000000000..5e9c9383c5
--- /dev/null
+++ b/binaries/data/mods/mod/gui/pregame/mainmenu.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/binaries/data/mods/public/gui/pregame/mainmenu.xml b/binaries/data/mods/public/gui/pregame/mainmenu.xml
index b104f2acf1..b78abe936a 100644
--- a/binaries/data/mods/public/gui/pregame/mainmenu.xml
+++ b/binaries/data/mods/public/gui/pregame/mainmenu.xml
@@ -294,7 +294,6 @@
type="button"
size="0 0 100% 28"
tooltip_style="pgToolTip"
- enabled="true"
>
Options
Adjust game settings.
@@ -351,6 +350,18 @@
]]>
+
+ Mod Selection
+ Select mods to use.
+
+ Engine.SwitchGuiPage("page_modmod.xml", {});
+
+
@@ -450,7 +461,7 @@
Game options and scenario design tools.
closeMenu();
- openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 4);
+ openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 5);
diff --git a/binaries/data/mods/public/mod.json b/binaries/data/mods/public/mod.json
new file mode 100644
index 0000000000..f818a50835
--- /dev/null
+++ b/binaries/data/mods/public/mod.json
@@ -0,0 +1,9 @@
+{
+ "name": "0ad",
+ "version": "0.0.17",
+ "label": "0 A.D. Empires Ascendant",
+ "url": "play0ad.com",
+ "description": "A free, open-source, historical RTS game.",
+ "dependencies": [],
+ "type": "game"
+}
diff --git a/build/resources/0ad.sh b/build/resources/0ad.sh
index 944f8c364f..63b14d9fc5 100644
--- a/build/resources/0ad.sh
+++ b/build/resources/0ad.sh
@@ -2,7 +2,7 @@
pyrogenesis=$(which pyrogenesis 2> /dev/null)
if [ -x "$pyrogenesis" ] ; then
- "$pyrogenesis" "$@"
+ "$pyrogenesis" -mod=public "$@"
else
echo "Error: pyrogenesis not found in ($PATH)"
exit 1
diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
index 43d442cd18..af49939da1 100644
--- a/source/gui/scripting/ScriptFunctions.cpp
+++ b/source/gui/scripting/ScriptFunctions.cpp
@@ -52,6 +52,7 @@
#include "ps/SavedGame.h"
#include "ps/scripting/JSInterface_ConfigDB.h"
#include "ps/scripting/JSInterface_Console.h"
+#include "ps/scripting/JSInterface_Mod.h"
#include "ps/scripting/JSInterface_VFS.h"
#include "ps/UserReport.h"
#include "ps/GameSetup/Atlas.h"
@@ -762,11 +763,11 @@ int GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
CScriptVal GetGUIObjectByName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStr name)
{
- IGUIObject* guiObj = g_GUI->FindObjectByName(name);
- if (guiObj)
- return OBJECT_TO_JSVAL(guiObj->GetJSObject());
- else
- return JSVAL_VOID;
+ IGUIObject* guiObj = g_GUI->FindObjectByName(name);
+ if (guiObj)
+ return OBJECT_TO_JSVAL(guiObj->GetJSObject());
+ else
+ return JSVAL_VOID;
}
// Return the date/time at which the current executable was compiled.
@@ -909,6 +910,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
JSI_Renderer::RegisterScriptFunctions(scriptInterface);
JSI_Console::RegisterScriptFunctions(scriptInterface);
JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
+ JSI_Mod::RegisterScriptFunctions(scriptInterface);
JSI_Sound::RegisterScriptFunctions(scriptInterface);
JSI_L10n::RegisterScriptFunctions(scriptInterface);
diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp
index 562fb12b4c..e25a13aa26 100644
--- a/source/ps/GameSetup/GameSetup.cpp
+++ b/source/ps/GameSetup/GameSetup.cpp
@@ -412,8 +412,9 @@ std::vector& GetMods(const CmdLineArgs& args, int flags)
}
g_modsLoaded = args.GetMultiple("mod");
- // TODO: It would be nice to remove this hard-coding of public
+ // TODO: It would be nice to remove this hard-coding of public (remove it once mod is standalone)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
+ g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
diff --git a/source/ps/scripting/JSInterface_Mod.cpp b/source/ps/scripting/JSInterface_Mod.cpp
new file mode 100644
index 0000000000..88ca91d17e
--- /dev/null
+++ b/source/ps/scripting/JSInterface_Mod.cpp
@@ -0,0 +1,128 @@
+/* Copyright (C) 2014 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 .
+ */
+
+#include "precompiled.h"
+
+#include "scriptinterface/ScriptInterface.h"
+#include "scriptinterface/ScriptVal.h"
+
+#include "lib/file/file_system.h"
+#include "lib/file/vfs/vfs.h"
+#include "lib/utf8.h"
+#include "ps/Filesystem.h"
+#include "ps/GameSetup/GameSetup.h"
+#include "ps/GameSetup/Paths.h"
+#include "ps/Mod.h"
+#include "ps/scripting/JSInterface_Mod.h"
+
+#include
+
+extern void restart_engine();
+
+/**
+ * Returns a JS object containing a listing of available mods that
+ * have a modname.json file in their modname folder. The returned
+ * object looks like { modname1: json1, modname2: json2, ... } where
+ * jsonN is the content of the modnameN/modnameN.json file as a JS
+ * object.
+ *
+ * @return JS object with available mods as the keys of the modname.json
+ * properties.
+ */
+CScriptVal JSI_Mod::GetAvailableMods(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+ JS::RootedObject obj(cx, JS_NewObject(cx, NULL, NULL, NULL));
+
+ const Paths paths(g_args);
+
+ // loop over all possible paths
+ OsPath modPath = paths.RData()/"mods";
+ OsPath modUserPath = paths.UserData()/"mods";
+
+ DirectoryNames modDirs;
+ DirectoryNames modDirsUser;
+
+ GetDirectoryEntries(modPath, NULL, &modDirs);
+ // Sort modDirs so that we can do a fast lookup below
+ std::sort(modDirs.begin(), modDirs.end());
+
+ PIVFS vfs = CreateVfs(1); // No cache needed; TODO but 0 crashes
+
+ for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter)
+ {
+ vfs->Clear();
+ if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
+ continue;
+
+ CVFSFile modinfo;
+ if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
+ continue;
+
+ JS::RootedValue json(cx);
+ scriptInterface->ParseJSON(modinfo.GetAsString(), &json);
+
+ // Valid mod, add it to our structure
+ JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json.address());
+ }
+
+ GetDirectoryEntries(modUserPath, NULL, &modDirsUser);
+ bool dev = InDevelopmentCopy();
+
+ for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter)
+ {
+ // If we are in a dev copy we do not mount mods in the user mod folder that
+ // are already present in the mod folder, thus we skip those here.
+ if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter))
+ continue;
+
+ vfs->Clear();
+ if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
+ continue;
+
+ CVFSFile modinfo;
+ if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
+ continue;
+
+ JS::RootedValue json(cx);
+ scriptInterface->ParseJSON(modinfo.GetAsString(), &json);
+
+ // Valid mod, add it to our structure
+ JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json.address());
+ }
+
+ return JS::ObjectValue(*obj);
+}
+
+void JSI_Mod::RestartEngine(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+{
+ restart_engine();
+}
+
+void JSI_Mod::SetMods(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::vector mods)
+{
+ g_modsLoaded = mods;
+}
+
+void JSI_Mod::RegisterScriptFunctions(ScriptInterface& scriptInterface)
+{
+ scriptInterface.RegisterFunction("GetAvailableMods");
+ scriptInterface.RegisterFunction("RestartEngine");
+ scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods");
+}
diff --git a/source/ps/scripting/JSInterface_Mod.h b/source/ps/scripting/JSInterface_Mod.h
new file mode 100644
index 0000000000..2e24dd532c
--- /dev/null
+++ b/source/ps/scripting/JSInterface_Mod.h
@@ -0,0 +1,32 @@
+/* Copyright (C) 2014 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_JSI_MOD
+#define INCLUDED_JSI_MOD
+
+class ScriptInterface;
+class CScriptVal;
+
+namespace JSI_Mod
+{
+ void RegisterScriptFunctions(ScriptInterface& scriptInterface);
+ CScriptVal GetAvailableMods(ScriptInterface::CxPrivate* pCxPrivate);
+ void RestartEngine(ScriptInterface::CxPrivate* pCxPrivate);
+ void SetMods(ScriptInterface::CxPrivate* pCxPrivate, std::vector mods);
+}
+
+#endif
diff --git a/source/tools/dist/0ad.nsi b/source/tools/dist/0ad.nsi
index f4b3c83afe..212c045b3c 100644
--- a/source/tools/dist/0ad.nsi
+++ b/source/tools/dist/0ad.nsi
@@ -58,6 +58,7 @@
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN $INSTDIR\binaries\system\pyrogenesis.exe
+ !define MUI_FINISHPAGE_RUN_PARAMETERS "-mod=public"
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
@@ -80,6 +81,8 @@ Section "!Game and data files" GameSection
SetOutPath "$INSTDIR\binaries\data\mods\public"
File "${CHECKOUTPATH}\binaries\data\mods\public\public.zip"
+ SetOutPath "$INSTDIR\binaries\data\mods\mod"
+ File "${CHECKOUTPATH}\binaries\data\mods\mod\mod.zip"
;Store installation folder
WriteRegStr SHCTX "Software\0 A.D." "" $INSTDIR
@@ -106,7 +109,8 @@ Section "!Game and data files" GameSection
;Create shortcuts
CreateDirectory "$SMPROGRAMS\$StartMenuFolder"
SetOutPath "$INSTDIR\binaries\system" ;Set working directory of shortcuts
- CreateShortCut "$SMPROGRAMS\$StartMenuFolder\0 A.D..lnk" "$INSTDIR\binaries\system\pyrogenesis.exe" ""
+ CreateShortCut "$SMPROGRAMS\$StartMenuFolder\0 A.D..lnk" "$INSTDIR\binaries\system\pyrogenesis.exe" "-mod=public"
+ CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Pyrogenesis mod selector.lnk" "$INSTDIR\binaries\system\pyrogenesis.exe" ""
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Map editor.lnk" "$INSTDIR\binaries\system\pyrogenesis.exe" "-editor" "$INSTDIR\binaries\data\tools\atlas\icons\ScenarioEditor.ico"
SetOutPath "$INSTDIR"
CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Open logs folder.lnk" "$INSTDIR\OpenLogsFolder.bat"
diff --git a/source/tools/dist/build.sh b/source/tools/dist/build.sh
index 9f20914830..900483cc21 100755
--- a/source/tools/dist/build.sh
+++ b/source/tools/dist/build.sh
@@ -32,8 +32,10 @@ echo L\"${SVNREV}-release\" > export-win32/build/svn_revision/svn_revision.txt
# Package the mod data
# (The platforms differ only in line endings, so just do the Unix one instead of
# generating two needlessly inconsistent packages)
-${EXE} -archivebuild=export-unix/binaries/data/mods/public -archivebuild-output=export-unix/binaries/data/mods/public/public.zip
+${EXE} -mod=mod -archivebuild=export-unix/binaries/data/mods/public -archivebuild-output=export-unix/binaries/data/mods/public/public.zip
cp export-unix/binaries/data/mods/public/public.zip export-win32/binaries/data/mods/public/public.zip
+${EXE} -archivebuild=export-unix/binaries/data/mods/mod -archivebuild-output=export-unix/binaries/data/mods/public/mod/mod.zip
+cp export-unix/binaries/data/mods/mod/mod.zip export-win32/binaries/data/mods/mod/mod.zip
# Collect the relevant files
ln -Tsf export-unix ${PREFIX}
@@ -44,7 +46,7 @@ tar cf $PREFIX-unix-build.tar \
${PREFIX}/{source,build,libraries/source,binaries/system/readme.txt,binaries/data/l10n,binaries/data/tests,binaries/data/mods/_test.*,*.txt}
tar cf $PREFIX-unix-data.tar \
--exclude='binaries/data/config/dev.cfg' \
- ${PREFIX}/binaries/data/{config,mods/public/public.zip,tools}
+ ${PREFIX}/binaries/data/{config,mods/mod/mod.zip,mods/public/public.zip,tools}
# TODO: ought to include generated docs in here, perhaps?
# Compress