Add automatic dependency downloading for Thunderstore mods and support for mods requiring DLLs in BepInEx core or patchers folders

This commit is contained in:
2026-04-14 16:56:45 +02:00
parent 44da1214dd
commit caee674448
2 changed files with 141 additions and 35 deletions

142
main.js
View File

@@ -43,6 +43,8 @@ let onlineCachedModList;
let onlineTotalModsCount; let onlineTotalModsCount;
let thunderstoreCachedModList; let thunderstoreCachedModList;
let thunderstoreTotalModsCount; let thunderstoreTotalModsCount;
let allThunderstoreCachedModList;
let allThunderstoreCachedModListNeedRefresh = true;
const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll", "libdoorstop.so", "run_bepinex.sh"]; const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll", "libdoorstop.so", "run_bepinex.sh"];
let bepinexVersion = bepinexStore.get("bepinex-version"); let bepinexVersion = bepinexStore.get("bepinex-version");
@@ -104,7 +106,8 @@ app.whenReady().then(async () => {
if (gotTheLock) { if (gotTheLock) {
createNexus(loadNexusApi()); createNexus(loadNexusApi());
checkInstalledMods(); await checkInstalledMods();
await checkForCoreAndPatcherMods();
createWindow(); createWindow();
} }
@@ -173,13 +176,14 @@ async function verifyUpdate() {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
///////////////// SAVING AND LOADING ///////////////// ///////////////// SAVING AND LOADING /////////////////
ipcMain.handle("save-path", (event, path) => { ipcMain.handle("save-path", async (event, path) => {
saveSilksongPath(path); await saveSilksongPath(path);
}); });
function saveSilksongPath(path) { async function saveSilksongPath(path) {
store.set("silksong-path", path); store.set("silksong-path", path);
checkInstalledMods(); await checkInstalledMods();
await checkForCoreAndPatcherMods();
} }
function loadSilksongPath() { function loadSilksongPath() {
@@ -405,7 +409,8 @@ async function installBepinex() {
saveBepinexVersion(release.tag_name); saveBepinexVersion(release.tag_name);
} }
checkInstalledMods(); await checkInstalledMods();
await checkForCoreAndPatcherMods();
} }
ipcMain.handle("install-bepinex", async () => { ipcMain.handle("install-bepinex", async () => {
@@ -443,6 +448,8 @@ async function backupBepinex() {
const bepinexFolderPath = path.join(silksongPath, "BepInEx"); const bepinexFolderPath = path.join(silksongPath, "BepInEx");
const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup"); const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
const BepinexPluginsPath = path.join(silksongPath, "BepInEx", "plugins"); const BepinexPluginsPath = path.join(silksongPath, "BepInEx", "plugins");
const bepinexCorePath = path.join(silksongPath, "BepInEx", "core", "custom");
const bepinexPatcherPath = path.join(silksongPath, "BepInEx", "patchers", "custom");
if (!(await fileExists(silksongPath))) { if (!(await fileExists(silksongPath))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning"); mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
@@ -453,9 +460,15 @@ async function backupBepinex() {
await fs.mkdir(bepinexBackupPath); await fs.mkdir(bepinexBackupPath);
} }
if (fileExists(BepinexPluginsPath)) { if (await fileExists(BepinexPluginsPath)) {
await fs.rm(BepinexPluginsPath, { recursive: true }); await fs.rm(BepinexPluginsPath, { recursive: true });
} }
if (await fileExists(bepinexCorePath)) {
await fs.rm(bepinexCorePath, { recursive: true });
}
if (await fileExists(bepinexPatcherPath)) {
await fs.rm(bepinexPatcherPath, { recursive: true });
}
if (await fileExists(bepinexFolderPath)) { if (await fileExists(bepinexFolderPath)) {
await fs.cp(bepinexFolderPath, path.join(bepinexBackupPath, "BepInEx"), { await fs.cp(bepinexFolderPath, path.join(bepinexBackupPath, "BepInEx"), {
@@ -681,6 +694,7 @@ ipcMain.handle("search-thunderstore-mods", async (event, keywords, offset, count
}); });
async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") { async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") {
if (allThunderstoreCachedModListNeedRefresh) {
const res = await fetch("https://thunderstore.io/c/hollow-knight-silksong/api/v1/package/", { const res = await fetch("https://thunderstore.io/c/hollow-knight-silksong/api/v1/package/", {
headers: { headers: {
"User-Agent": userAgent, "User-Agent": userAgent,
@@ -688,15 +702,7 @@ async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilt
}); });
let modsInfo = await res.json(); let modsInfo = await res.json();
const modsToRemove = [ const modsToRemove = ["f21c391c-0bc5-431d-a233-95323b95e01b", "42f76853-d2a4-4520-949b-13a02fdbbbcb", "34eac80c-5497-470e-b98c-f53421b828c0"];
"f21c391c-0bc5-431d-a233-95323b95e01b",
"58c9e43a-549d-4d49-a576-4eed36775a84",
"0fc63a3b-3c69-4be5-85f8-bb127eec81b3",
"d5419c5d-c22a-4a47-b73d-ba4101f28635",
"d5b65a03-1217-4496-8af6-8dda4d763676",
"42f76853-d2a4-4520-949b-13a02fdbbbcb",
"34eac80c-5497-470e-b98c-f53421b828c0",
];
let reMappedModsInfo = []; let reMappedModsInfo = [];
for (let i = 0; i < modsInfo.length; i++) { for (let i = 0; i < modsInfo.length; i++) {
modsInfo[i].source = "thunderstore"; modsInfo[i].source = "thunderstore";
@@ -707,8 +713,17 @@ async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilt
} }
reMappedModsInfo.push(reMapThunderstoreModsInfo(modsInfo[i])); reMappedModsInfo.push(reMapThunderstoreModsInfo(modsInfo[i]));
} }
allThunderstoreCachedModList = reMappedModsInfo;
allThunderstoreCachedModListNeedRefresh = false;
setTimeout(
() => {
allThunderstoreCachedModListNeedRefresh = true;
},
10 * 60 * 1000,
);
}
const result = sortAndFilterModsList(reMappedModsInfo, keywords, offset, count, sortFilter, sortOrder); const result = sortAndFilterModsList(allThunderstoreCachedModList, keywords, offset, count, sortFilter, sortOrder);
thunderstoreCachedModList = result.list; thunderstoreCachedModList = result.list;
thunderstoreTotalModsCount = result.totalCount; thunderstoreTotalModsCount = result.totalCount;
} }
@@ -732,10 +747,15 @@ function reMapThunderstoreModsInfo(modInfo) {
downloads: totalDownloads, downloads: totalDownloads,
fileSize: modInfo.versions[0].file_size, fileSize: modInfo.versions[0].file_size,
source: modInfo.source, source: modInfo.source,
dependencies: modInfo.versions[0].dependencies,
}; };
} }
ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => { ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => {
await downloadThunderstoreMods(url, modId);
});
async function downloadThunderstoreMods(url, modId) {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx"); const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
if (!(await fileExists(loadSilksongPath()))) { if (!(await fileExists(loadSilksongPath()))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning"); mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
@@ -752,9 +772,29 @@ ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => {
} }
saveModInfo(modId); saveModInfo(modId);
await downloadThunderstoreModsDependencies(modId);
await checkForCoreAndPatcherMods();
mainWindow.webContents.send("showToast", "Mod downloaded successfully."); mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
installedCachedModList = undefined; installedCachedModList = undefined;
}); }
async function downloadThunderstoreModsDependencies(modId) {
const dependencies = allThunderstoreCachedModList.find((mod) => mod.modId == modId).dependencies;
for (const dependency of dependencies) {
const dependencyArray = dependency.split("-");
const modInfo = allThunderstoreCachedModList.find((mod) => mod.author === dependencyArray[0] && mod.name === dependencyArray[1]);
if (modInfo) {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
const url = `https://thunderstore.io/package/download/${dependencyArray[0]}/${dependencyArray[1]}/${dependencyArray[2]}`;
await downloadAndUnzip(url, path.join(modSavePath, modInfo.modId));
if (await fileExists(bepinexFolderPath)) {
await fs.cp(path.join(modSavePath, modInfo.modId), path.join(bepinexFolderPath, "plugins", modInfo.modId), { recursive: true });
}
saveModInfo(modInfo.modId, undefined, modInfo);
await downloadThunderstoreModsDependencies(modInfo.modId);
}
}
}
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
//////////////////////// MODS //////////////////////// //////////////////////// MODS ////////////////////////
@@ -814,6 +854,7 @@ ipcMain.handle("uninstall-mod", async (event, modId) => {
} }
saveModInfo(modId, true); saveModInfo(modId, true);
checkForCoreAndPatcherMods();
}); });
ipcMain.handle("activate-mod", async (event, modId) => { ipcMain.handle("activate-mod", async (event, modId) => {
@@ -836,6 +877,7 @@ async function activateMod(modId) {
await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true }); await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true });
} }
} }
checkForCoreAndPatcherMods();
} }
ipcMain.handle("deactivate-mod", async (event, modId) => { ipcMain.handle("deactivate-mod", async (event, modId) => {
@@ -849,6 +891,7 @@ ipcMain.handle("deactivate-mod", async (event, modId) => {
await fs.rm(path.join(BepinexPluginsPath, String(modId)), { recursive: true }); await fs.rm(path.join(BepinexPluginsPath, String(modId)), { recursive: true });
} }
} }
checkForCoreAndPatcherMods();
} }
}); });
@@ -919,6 +962,69 @@ ipcMain.handle("add-offline-mod", async () => {
return true; return true;
}); });
async function checkForCoreAndPatcherMods() {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
const bepinexPluginsPath = path.join(bepinexFolderPath, "Plugins");
const bepinexCorePath = path.join(bepinexFolderPath, "core", "custom");
const bepinexPatcherPath = path.join(bepinexFolderPath, "patchers", "custom");
if (await fileExists(bepinexCorePath)) {
await fs.rm(bepinexCorePath, { recursive: true });
}
if (await fileExists(bepinexPatcherPath)) {
await fs.rm(bepinexPatcherPath, { recursive: true });
}
await fs.mkdir(bepinexCorePath, { recursive: true });
await fs.mkdir(bepinexPatcherPath, { recursive: true });
async function scanDir(dirPath, relativePath = "") {
let entries;
try {
entries = await fs.readdir(dirPath, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const currentRelative = path.join(relativePath, entry.name);
if (entry.name === "core") {
await copyContents(path.join(dirPath, entry.name), bepinexCorePath);
if (await fileExists(path.join(bepinexPluginsPath, currentRelative))) {
await fs.rm(path.join(bepinexPluginsPath, currentRelative), { recursive: true });
}
} else if (entry.name === "patchers") {
await copyContents(path.join(dirPath, entry.name), bepinexPatcherPath);
if (await fileExists(path.join(bepinexPluginsPath, currentRelative))) {
await fs.rm(path.join(bepinexPluginsPath, currentRelative), { recursive: true });
}
} else {
if (!(await fileExists(path.join(bepinexPluginsPath, currentRelative)))) continue;
await scanDir(path.join(dirPath, entry.name), currentRelative);
}
}
}
async function copyContents(srcDir, destDir) {
let files;
try {
files = await fs.readdir(srcDir);
} catch {
return;
}
for (const file of files) {
const src = path.join(srcDir, file);
const dest = path.join(destDir, file);
await fs.copyFile(src, dest);
}
}
await scanDir(modSavePath);
}
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
//////////////////// UNCATEGORIZE //////////////////// //////////////////// UNCATEGORIZE ////////////////////

View File

@@ -288,7 +288,7 @@ async function navigate(page) {
electronAPI.openExternalLink(modLink); electronAPI.openExternalLink(modLink);
}); });
modDownloadButton = modTemplateCopy.getElementById("download-mod-button"); const modDownloadButton = modTemplateCopy.getElementById("download-mod-button");
modDownloadButton.addEventListener("click", function (event) { modDownloadButton.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
const modDownloadLink = `${modUrl}?tab=files`; const modDownloadLink = `${modUrl}?tab=files`;
@@ -372,7 +372,7 @@ async function navigate(page) {
electronAPI.openExternalLink(modLink); electronAPI.openExternalLink(modLink);
}); });
modDownloadButton = modTemplateCopy.getElementById("download-mod-button"); const modDownloadButton = modTemplateCopy.getElementById("download-mod-button");
modDownloadButton.addEventListener("click", function (event) { modDownloadButton.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
const modDownloadLink = `https://thunderstore.io/package/download/${mod.author}/${mod.name}/${mod.version}`; const modDownloadLink = `https://thunderstore.io/package/download/${mod.author}/${mod.name}/${mod.version}`;
@@ -610,7 +610,7 @@ async function searchInstalledMods() {
//////////////// NEXUS / THUNDERSTORE //////////////// //////////////// NEXUS / THUNDERSTORE ////////////////
async function verifyNexusAPI() { async function verifyNexusAPI() {
response = await nexus.verifyAPI(); const response = await nexus.verifyAPI();
const nexusCheckImage = document.getElementById("nexus-check-image"); const nexusCheckImage = document.getElementById("nexus-check-image");
if (nexusCheckImage == undefined) { if (nexusCheckImage == undefined) {