mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-17 05:44:08 -07:00
505 lines
13 KiB
JavaScript
505 lines
13 KiB
JavaScript
/*
|
|
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", // optional
|
|
url: "http://play0ad.wfg.com/", //optional
|
|
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("Folder"), translate("Label"), 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()
|
|
{
|
|
let mods = Engine.GetAvailableMods();
|
|
let keys = ["name", "label", "description", "dependencies", "version"];
|
|
Object.keys(mods).forEach(function(k) {
|
|
for (let i = 0; i < keys.length; ++i)
|
|
if (!keys[i] in mods[k])
|
|
{
|
|
log("Skipping mod '"+k+"'. Missing property '"+keys[i]+"'.");
|
|
return;
|
|
}
|
|
|
|
g_mods[k] = mods[k];
|
|
});
|
|
|
|
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 [keys, 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;
|
|
|
|
keys.push(foldername);
|
|
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 = keys;
|
|
|
|
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 != translate("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)
|
|
{
|
|
var obj = Engine.GetGUIObjectByName(objectName);
|
|
if (!obj)
|
|
return;
|
|
|
|
var idx = obj.selected;
|
|
if (idx === -1)
|
|
return;
|
|
|
|
var num = obj.list.length;
|
|
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.
|
|
obj.list = g_modsEnabled;
|
|
obj.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)
|
|
{
|
|
var listObject = Engine.GetGUIObjectByName(listObjectName);
|
|
if (listObject.selected == -1)
|
|
var desc = '[color="255 100 100"]' + translate("No mod has been selected.") + '[/color]';
|
|
else
|
|
{
|
|
let key = listObject.list[listObject.selected];
|
|
var desc = g_mods[key].description;
|
|
}
|
|
|
|
Engine.GetGUIObjectByName("globalModDescription").caption = desc;
|
|
}
|