Files
0ad/binaries/data/mods/mod/gui/modio/modio.js
T
Angen 6f1d17c954 Stop failing if mod is broken and display invalid mods in downloader [Mod Io]
If one mod is not signed or broken in any other way, mod.io downloader
fails and does not display any mod.
The problem with unsigned mod is that its metadata are empty.

That means one cannot break mod io downloader with mods having invalid
data.

mark mod as invalid and display in list as disabled and display reason
instead description to not spam ugly error messages on screen as this is
not error by the game but of the moder
report failed signatures back to list of mods
fail if property is not set using strict mode when getting from js
check in js for undefined values

Allow to filter only valid mods.

Differential Revision: https://code.wildfiregames.com/D2114
Reviewed by: @Itms
Fixes: #5459

This was SVN commit r23821.
2020-07-12 09:25:03 +00:00

358 lines
11 KiB
JavaScript

var g_ModsAvailableOnline = [];
/**
* Indicates if we have encountered an error in one of the network-interaction attempts.
*
* We use a global so we don't get multiple messageBoxes appearing (one for each "tick").
*
* Set to `true` by showErrorMessageBox
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
*/
var g_Failure;
/**
* Indicates if the user has cancelled a request.
*
* Primarily used so the user can cancel the mod list fetch, as whenever that get cancelled,
* the modio state reverts to "ready", even if we've successfully listed mods before.
*
* Set to `true` by cancelRequest
* Set to `false` by updateModList, and downloadFile
*/
var g_RequestCancelled;
var g_RequestStartTime;
/**
* Returns true if ModIoAdvanceRequest should be called.
*/
var g_ModIOState = {
/**
* Finished status indicators
*/
"ready": progressData => {
// GameID acquired, ready to fetch mod list
if (!g_RequestCancelled)
updateModList();
return true;
},
"listed": progressData => {
// List of available mods acquired
// Only run this once (for each update).
if (Engine.GetGUIObjectByName("modsAvailableList").list.length)
return true;
hideDialog();
Engine.GetGUIObjectByName("refreshButton").enabled = true;
g_ModsAvailableOnline = Engine.ModIoGetMods();
displayMods();
return true;
},
"success": progressData => {
// Successfully acquired a mod file
hideDialog();
Engine.GetGUIObjectByName("downloadButton").enabled = true;
return true;
},
/**
* In-progress status indicators.
*/
"gameid": progressData => {
// Acquiring GameID from mod.io
return true;
},
"listing": progressData => {
// Acquiring list of available mods from mod.io
return true;
},
"downloading": progressData => {
// Downloading a mod file
updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize);
return true;
},
/**
* Error/Failure status indicators.
*/
"failed_gameid": progressData => {
// Game ID couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Game ID could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Initialization Error"),
[translate("Abort"), translate("Retry")],
[closePage, init]);
return false;
},
"failed_listing": progressData => {
// Mod list couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Fetch Error"),
[translate("Abort"), translate("Retry")],
[cancelModListUpdate, updateModList]);
return false;
},
"failed_downloading": progressData => {
// File couldn't be retrieved
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File download failed.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Download Error"),
[translate("Abort"), translate("Retry")],
[cancelRequest, downloadMod]);
return false;
},
"failed_filecheck": progressData => {
// The file is corrupted
showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File verification error.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Verification Error"),
[translate("Abort")],
[cancelRequest]);
return false;
},
/**
* Default
*/
"none": progressData => {
// Nothing has happened yet.
return true;
}
};
function init(data)
{
progressDialog(
translate("Initializing mod.io interface."),
translate("Initializing"),
false,
translate("Cancel"),
closePage);
g_Failure = false;
Engine.ModIoStartGetGameId();
}
function onTick()
{
let progressData = Engine.ModIoGetDownloadProgress();
let handler = g_ModIOState[progressData.status];
if (!handler)
{
warn("Unrecognized progress status: " + progressData.status);
return;
}
if (handler(progressData))
Engine.ModIoAdvanceRequest();
}
function displayMods()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
let selectedMod = modsAvailableList.list[modsAvailableList.selected];
modsAvailableList.selected = -1;
let displayedMods = clone(g_ModsAvailableOnline);
for (let i = 0; i < displayedMods.length; ++i)
displayedMods[i].i = i;
let filterColumns = ["name", "name_id", "summary"];
let filterText = Engine.GetGUIObjectByName("modFilter").caption.toLowerCase();
if (Engine.GetGUIObjectByName("compatibilityFilter").checked)
displayedMods = displayedMods.filter(mod => !mod.invalid);
displayedMods = displayedMods.filter(mod => filterColumns.some(column => mod[column].toLowerCase().indexOf(filterText) != -1));
displayedMods.sort((mod1, mod2) =>
modsAvailableList.selected_column_order *
(modsAvailableList.selected_column == "filesize" ?
mod1.filesize - mod2.filesize :
String(mod1[modsAvailableList.selected_column]).localeCompare(String(mod2[modsAvailableList.selected_column]))));
modsAvailableList.list_name = displayedMods.map(mod => compatibilityColor(mod.name, !mod.invalid));
modsAvailableList.list_name_id = displayedMods.map(mod => compatibilityColor(mod.name_id, !mod.invalid));
modsAvailableList.list_version = displayedMods.map(mod => compatibilityColor(mod.version || "", !mod.invalid));
modsAvailableList.list_filesize = displayedMods.map(mod => compatibilityColor(mod.filesize !== undefined ? filesizeToString(mod.filesize) : filesizeToString(mod.filesize), !mod.invalid));
modsAvailableList.list_dependencies = displayedMods.map(mod => compatibilityColor((mod.dependencies || []).join(" "), !mod.invalid));
modsAvailableList.list = displayedMods.map(mod => mod.i);
modsAvailableList.selected = modsAvailableList.list.indexOf(selectedMod);
}
function clearModList()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
modsAvailableList.selected = -1;
for (let listIdx of Object.keys(modsAvailableList).filter(key => key.startsWith("list")))
modsAvailableList[listIdx] = [];
}
function selectedModIndex()
{
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
if (modsAvailableList.selected == -1)
return undefined;
return +modsAvailableList.list[modsAvailableList.selected];
}
function isSelectedModInvalid(selected)
{
return selected !== undefined && !!g_ModsAvailableOnline[selected].invalid && g_ModsAvailableOnline[selected].invalid == "true";
}
function showModDescription()
{
let selected = selectedModIndex();
let isSelected = selected !== undefined;
let isInvalid = isSelectedModInvalid(selected);
Engine.GetGUIObjectByName("downloadButton").enabled = isSelected && !isInvalid;
Engine.GetGUIObjectByName("modDescription").caption = isSelected && !isInvalid ? g_ModsAvailableOnline[selected].summary : "";
Engine.GetGUIObjectByName("modError").caption = isSelected && isInvalid ? sprintf(translate("Invalid mod: %(error)s"), {"error": g_ModsAvailableOnline[selected].error }) : "";
}
function cancelModListUpdate()
{
cancelRequest();
if (!g_ModsAvailableOnline.length)
{
closePage();
return;
}
displayMods();
Engine.GetGUIObjectByName('refreshButton').enabled = true;
}
function updateModList()
{
clearModList();
Engine.GetGUIObjectByName("refreshButton").enabled = false;
progressDialog(
translate("Fetching and updating list of available mods."),
translate("Updating"),
false,
translate("Cancel Update"),
cancelModListUpdate);
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartListMods();
}
function downloadMod()
{
let selected = selectedModIndex();
if (isSelectedModInvalid(selected))
return;
progressDialog(
sprintf(translate("Downloading “%(modname)s”"), {
"modname": g_ModsAvailableOnline[selected].name
}),
translate("Downloading"),
true,
translate("Cancel Download"),
() => { Engine.GetGUIObjectByName("downloadButton").enabled = true; });
Engine.GetGUIObjectByName("downloadButton").enabled = false;
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartDownloadMod(selected);
}
function cancelRequest()
{
g_Failure = false;
g_RequestCancelled = true;
Engine.ModIoCancelRequest();
hideDialog();
}
function closePage()
{
Engine.PopGuiPage();
}
function showErrorMessageBox(caption, title, buttonCaptions, buttonActions)
{
if (g_Failure)
return;
messageBox(500, 250, caption, title, buttonCaptions, buttonActions);
g_Failure = true;
}
function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption, buttonAction)
{
Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle;
let downloadDialog_caption = Engine.GetGUIObjectByName("downloadDialog_caption");
downloadDialog_caption.caption = dialogCaption;
let size = downloadDialog_caption.size;
size.rbottom = showProgressBar ? 40 : 80;
downloadDialog_caption.size = size;
Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar;
Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar;
let downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button");
downloadDialog_button.caption = buttonCaption;
downloadDialog_button.onPress = () => { cancelRequest(); buttonAction(); };
Engine.GetGUIObjectByName("downloadDialog").hidden = false;
g_RequestStartTime = Date.now();
}
/*
* The "remaining time" and "average speed" texts both naively assume that
* the connection remains relatively stable throughout the download.
*/
function updateProgressBar(progress, totalSize)
{
let progressPercent = Math.ceil(progress * 100);
Engine.GetGUIObjectByName("downloadDialog_progressBar").progress = progressPercent;
let transferredSize = progress * totalSize;
let transferredSizeObj = filesizeToObj(transferredSize);
// Translation: Mod file download indicator. Current size over expected final size, with percentage complete.
Engine.GetGUIObjectByName("downloadDialog_progressText").caption = sprintf(translate("%(current)s / %(total)s (%(percent)s%%)"), {
"current": filesizeToObj(totalSize).unit == transferredSizeObj.unit ? transferredSizeObj.filesize : filesizeToString(transferredSize),
"total": filesizeToString(totalSize),
"percent": progressPercent
});
let elapsedTime = Date.now() - g_RequestStartTime;
let remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0;
let avgSpeed = filesizeToObj(transferredSize / (elapsedTime / 1000));
// Translation: Mod file download status message.
Engine.GetGUIObjectByName("downloadDialog_status").caption = sprintf(translate("Time Elapsed: %(elapsed)s\nEstimated Time Remaining: %(remaining)s\nAverage Speed: %(avgSpeed)s"), {
"elapsed": timeToString(elapsedTime),
"remaining": remainingTime ? timeToString(remainingTime) : translate("∞"),
// Translation: Average download speed, used to give the user a very rough and naive idea of the download time. For example: 123.4 KiB/s
"avgSpeed": sprintf(translate("%(number)s %(unit)s/s"), {
"number": avgSpeed.filesize,
"unit": avgSpeed.unit
})
});
}
function hideDialog()
{
Engine.GetGUIObjectByName("downloadDialog").hidden = true;
}