mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace style for JavaScript code. This plugin was however updated the last time over 9 years ago and will be incompatible with ESLint v10, as that [removes `context.getSourceCode()`][1], the plugin relies on. To keep the eslint config working with ESLint v10, this replaces `eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule from `@stylistic/eslint-plugin`, a package we already use. While `@stylistic/brace-style` doesn't offer an option to format braces in exactly the same way as before, the "allman" style seems to be the one closest to the existing code. [1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members [2]: https://eslint.style/rules/brace-style
385 lines
12 KiB
JavaScript
385 lines
12 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;
|
|
|
|
var g_ModIOState = {
|
|
/**
|
|
* Finished status indicators
|
|
*/
|
|
"ready": (progressData, closePageCallback) =>
|
|
{
|
|
// GameID acquired, ready to fetch mod list
|
|
if (!g_RequestCancelled)
|
|
updateModList(closePageCallback);
|
|
},
|
|
"listed": progressData =>
|
|
{
|
|
// List of available mods acquired
|
|
|
|
// Only run this once (for each update).
|
|
if (Engine.GetGUIObjectByName("modsAvailableList").list.length)
|
|
return;
|
|
|
|
hideDialog();
|
|
Engine.GetGUIObjectByName("refreshButton").enabled = true;
|
|
g_ModsAvailableOnline = Engine.ModIoGetMods();
|
|
displayMods();
|
|
},
|
|
"success": progressData =>
|
|
{
|
|
// Successfully acquired a mod file
|
|
hideDialog();
|
|
Engine.GetGUIObjectByName("downloadButton").enabled = true;
|
|
},
|
|
/**
|
|
* In-progress status indicators.
|
|
*/
|
|
"gameid": progressData =>
|
|
{
|
|
// Acquiring GameID from mod.io
|
|
},
|
|
"listing": progressData =>
|
|
{
|
|
// Acquiring list of available mods from mod.io
|
|
},
|
|
"downloading": progressData =>
|
|
{
|
|
// Downloading a mod file
|
|
updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize);
|
|
},
|
|
/**
|
|
* Error/Failure status indicators.
|
|
*/
|
|
"failed_gameid": async(progressData, closePageCallback) =>
|
|
{
|
|
// Game ID couldn't be retrieved
|
|
const promise = 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")]);
|
|
if (!promise)
|
|
return;
|
|
if (await promise === 0)
|
|
closePageCallback();
|
|
else
|
|
init();
|
|
},
|
|
"failed_listing": async(progressData, closePageCallback) =>
|
|
{
|
|
// Mod list couldn't be retrieved
|
|
const promise = 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")]);
|
|
if (!promise)
|
|
return;
|
|
if (await promise === 0)
|
|
cancelModListUpdate(closePageCallback);
|
|
else
|
|
updateModList(closePageCallback);
|
|
},
|
|
"failed_downloading": async(progressData) =>
|
|
{
|
|
// File couldn't be retrieved
|
|
const promise = 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")]);
|
|
if (!promise)
|
|
return;
|
|
if (await promise === 0)
|
|
cancelRequest();
|
|
else
|
|
downloadMod();
|
|
},
|
|
"failed_filecheck": async(progressData) =>
|
|
{
|
|
// The file is corrupted
|
|
const promise = 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")]);
|
|
if (!promise)
|
|
return;
|
|
await promise;
|
|
cancelRequest();
|
|
},
|
|
/**
|
|
* Default
|
|
*/
|
|
"none": progressData =>
|
|
{
|
|
// Nothing has happened yet.
|
|
}
|
|
};
|
|
|
|
function init(data)
|
|
{
|
|
const promise = progressDialog(
|
|
translate("Initializing mod.io interface."),
|
|
translate("Initializing"),
|
|
false,
|
|
translate("Cancel"));
|
|
|
|
g_Failure = false;
|
|
Engine.ModIoStartGetGameId();
|
|
|
|
return Promise.race([
|
|
promise,
|
|
new Promise(closePageCallback =>
|
|
{
|
|
Engine.GetGUIObjectByName("backButton").onPress = closePageCallback;
|
|
Engine.GetGUIObjectByName("modio").onTick = onTick.bind(null, closePageCallback);
|
|
})
|
|
]);
|
|
}
|
|
|
|
function onTick(closePageCallback)
|
|
{
|
|
const progressData = Engine.ModIoGetDownloadProgress();
|
|
|
|
const handler = g_ModIOState[progressData.status];
|
|
if (!handler)
|
|
{
|
|
warn("Unrecognized progress status: " + progressData.status);
|
|
return;
|
|
}
|
|
|
|
handler(progressData, closePageCallback);
|
|
if (!progressData.status.startsWith("failed"))
|
|
Engine.ModIoAdvanceRequest();
|
|
}
|
|
|
|
function displayMods()
|
|
{
|
|
const modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
|
|
const 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;
|
|
|
|
const filterColumns = ["name", "name_id", "summary"];
|
|
const 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()
|
|
{
|
|
const modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
|
|
modsAvailableList.selected = -1;
|
|
modsAvailableList.list_name = [];
|
|
modsAvailableList.list_name_id = [];
|
|
modsAvailableList.list_version = [];
|
|
modsAvailableList.list_filesize = [];
|
|
modsAvailableList.list_dependencies = [];
|
|
modsAvailableList.list = [];
|
|
}
|
|
|
|
function selectedModIndex()
|
|
{
|
|
const 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()
|
|
{
|
|
const selected = selectedModIndex();
|
|
const isSelected = selected !== undefined;
|
|
const 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(closePageCallback)
|
|
{
|
|
cancelRequest();
|
|
|
|
if (!g_ModsAvailableOnline.length)
|
|
{
|
|
closePageCallback();
|
|
return;
|
|
}
|
|
|
|
displayMods();
|
|
Engine.GetGUIObjectByName('refreshButton').enabled = true;
|
|
}
|
|
|
|
async function updateModList(closePageCallback)
|
|
{
|
|
clearModList();
|
|
Engine.GetGUIObjectByName("refreshButton").enabled = false;
|
|
|
|
const promise = progressDialog(
|
|
translate("Fetching and updating list of available mods."),
|
|
translate("Updating"),
|
|
false,
|
|
translate("Cancel Update"));
|
|
|
|
g_Failure = false;
|
|
g_RequestCancelled = false;
|
|
Engine.ModIoStartListMods();
|
|
|
|
await promise;
|
|
cancelModListUpdate(closePageCallback);
|
|
}
|
|
|
|
async function downloadMod()
|
|
{
|
|
const selected = selectedModIndex();
|
|
|
|
if (isSelectedModInvalid(selected))
|
|
return;
|
|
|
|
const promise = progressDialog(
|
|
sprintf(translate("Downloading “%(modname)s”"), {
|
|
"modname": g_ModsAvailableOnline[selected].name
|
|
}),
|
|
translate("Downloading"),
|
|
true,
|
|
translate("Cancel Download"));
|
|
|
|
Engine.GetGUIObjectByName("downloadButton").enabled = false;
|
|
|
|
g_Failure = false;
|
|
g_RequestCancelled = false;
|
|
Engine.ModIoStartDownloadMod(selected);
|
|
|
|
await promise;
|
|
Engine.GetGUIObjectByName("downloadButton").enabled = true;
|
|
}
|
|
|
|
function cancelRequest()
|
|
{
|
|
g_Failure = false;
|
|
g_RequestCancelled = true;
|
|
Engine.ModIoCancelRequest();
|
|
hideDialog();
|
|
}
|
|
|
|
function showErrorMessageBox(caption, title, buttonCaptions)
|
|
{
|
|
if (g_Failure)
|
|
return null;
|
|
g_Failure = true;
|
|
|
|
return messageBox(500, 250, caption, title, buttonCaptions);
|
|
}
|
|
|
|
async function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption)
|
|
{
|
|
Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle;
|
|
|
|
const downloadDialogCaption = Engine.GetGUIObjectByName("downloadDialog_caption");
|
|
downloadDialogCaption.caption = dialogCaption;
|
|
downloadDialogCaption.size.rbottom = showProgressBar ? 40 : 80;
|
|
|
|
Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar;
|
|
Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar;
|
|
|
|
Engine.GetGUIObjectByName("downloadDialog").hidden = false;
|
|
|
|
g_RequestStartTime = Date.now();
|
|
|
|
const downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button");
|
|
downloadDialog_button.caption = buttonCaption;
|
|
await new Promise(resolve =>
|
|
{
|
|
downloadDialog_button.onPress = resolve;
|
|
});
|
|
cancelRequest();
|
|
}
|
|
|
|
/*
|
|
* The "remaining time" and "average speed" texts both naively assume that
|
|
* the connection remains relatively stable throughout the download.
|
|
*/
|
|
function updateProgressBar(progress, totalSize)
|
|
{
|
|
const progressPercent = Math.ceil(progress * 100);
|
|
Engine.GetGUIObjectByName("downloadDialog_progressBar").progress = progressPercent;
|
|
|
|
const transferredSize = progress * totalSize;
|
|
const 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
|
|
});
|
|
|
|
const elapsedTime = Date.now() - g_RequestStartTime;
|
|
const remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0;
|
|
const 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;
|
|
}
|