From 64bfa089afc6d14882bcc6b7f4ef99a81f694da3 Mon Sep 17 00:00:00 2001 From: leper Date: Mon, 25 Aug 2014 16:02:54 +0000 Subject: [PATCH] Add mod selection mod. Includes some contributions by rada and sanderd17. This was SVN commit r15677. --- binaries/data/config/default.cfg | 3 + binaries/data/mods/mod/gui/modmod/modmod.js | 494 ++++++++++++++++++ binaries/data/mods/mod/gui/modmod/modmod.xml | 192 +++++++ binaries/data/mods/mod/gui/modmod/styles.xml | 15 + binaries/data/mods/mod/gui/page_modmod.xml | 21 + binaries/data/mods/mod/gui/page_pregame.xml | 4 + .../data/mods/mod/gui/pregame/mainmenu.js | 4 + .../data/mods/mod/gui/pregame/mainmenu.xml | 5 + .../data/mods/public/gui/pregame/mainmenu.xml | 15 +- binaries/data/mods/public/mod.json | 9 + build/resources/0ad.sh | 2 +- source/gui/scripting/ScriptFunctions.cpp | 12 +- source/ps/GameSetup/GameSetup.cpp | 3 +- source/ps/scripting/JSInterface_Mod.cpp | 128 +++++ source/ps/scripting/JSInterface_Mod.h | 32 ++ source/tools/dist/0ad.nsi | 6 +- source/tools/dist/build.sh | 6 +- 17 files changed, 939 insertions(+), 12 deletions(-) create mode 100644 binaries/data/mods/mod/gui/modmod/modmod.js create mode 100644 binaries/data/mods/mod/gui/modmod/modmod.xml create mode 100644 binaries/data/mods/mod/gui/modmod/styles.xml create mode 100644 binaries/data/mods/mod/gui/page_modmod.xml create mode 100644 binaries/data/mods/mod/gui/page_pregame.xml create mode 100644 binaries/data/mods/mod/gui/pregame/mainmenu.js create mode 100644 binaries/data/mods/mod/gui/pregame/mainmenu.xml create mode 100644 binaries/data/mods/public/mod.json create mode 100644 source/ps/scripting/JSInterface_Mod.cpp create mode 100644 source/ps/scripting/JSInterface_Mod.h 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 @@ + + + +