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 thunderstoreCachedModList;
let thunderstoreTotalModsCount;
let allThunderstoreCachedModList;
let allThunderstoreCachedModListNeedRefresh = true;
const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll", "libdoorstop.so", "run_bepinex.sh"];
let bepinexVersion = bepinexStore.get("bepinex-version");
@@ -104,7 +106,8 @@ app.whenReady().then(async () => {
if (gotTheLock) {
createNexus(loadNexusApi());
checkInstalledMods();
await checkInstalledMods();
await checkForCoreAndPatcherMods();
createWindow();
}
@@ -173,13 +176,14 @@ async function verifyUpdate() {
//////////////////////////////////////////////////////
///////////////// SAVING AND LOADING /////////////////
ipcMain.handle("save-path", (event, path) => {
saveSilksongPath(path);
ipcMain.handle("save-path", async (event, path) => {
await saveSilksongPath(path);
});
function saveSilksongPath(path) {
async function saveSilksongPath(path) {
store.set("silksong-path", path);
checkInstalledMods();
await checkInstalledMods();
await checkForCoreAndPatcherMods();
}
function loadSilksongPath() {
@@ -405,7 +409,8 @@ async function installBepinex() {
saveBepinexVersion(release.tag_name);
}
checkInstalledMods();
await checkInstalledMods();
await checkForCoreAndPatcherMods();
}
ipcMain.handle("install-bepinex", async () => {
@@ -443,6 +448,8 @@ async function backupBepinex() {
const bepinexFolderPath = path.join(silksongPath, "BepInEx");
const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
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))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
@@ -453,9 +460,15 @@ async function backupBepinex() {
await fs.mkdir(bepinexBackupPath);
}
if (fileExists(BepinexPluginsPath)) {
if (await fileExists(BepinexPluginsPath)) {
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)) {
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") {
if (allThunderstoreCachedModListNeedRefresh) {
const res = await fetch("https://thunderstore.io/c/hollow-knight-silksong/api/v1/package/", {
headers: {
"User-Agent": userAgent,
@@ -688,15 +702,7 @@ async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilt
});
let modsInfo = await res.json();
const modsToRemove = [
"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",
];
const modsToRemove = ["f21c391c-0bc5-431d-a233-95323b95e01b", "42f76853-d2a4-4520-949b-13a02fdbbbcb", "34eac80c-5497-470e-b98c-f53421b828c0"];
let reMappedModsInfo = [];
for (let i = 0; i < modsInfo.length; i++) {
modsInfo[i].source = "thunderstore";
@@ -707,8 +713,17 @@ async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilt
}
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;
thunderstoreTotalModsCount = result.totalCount;
}
@@ -732,10 +747,15 @@ function reMapThunderstoreModsInfo(modInfo) {
downloads: totalDownloads,
fileSize: modInfo.versions[0].file_size,
source: modInfo.source,
dependencies: modInfo.versions[0].dependencies,
};
}
ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => {
await downloadThunderstoreMods(url, modId);
});
async function downloadThunderstoreMods(url, modId) {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
if (!(await fileExists(loadSilksongPath()))) {
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);
await downloadThunderstoreModsDependencies(modId);
await checkForCoreAndPatcherMods();
mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
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 ////////////////////////
@@ -814,6 +854,7 @@ ipcMain.handle("uninstall-mod", async (event, modId) => {
}
saveModInfo(modId, true);
checkForCoreAndPatcherMods();
});
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 });
}
}
checkForCoreAndPatcherMods();
}
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 });
}
}
checkForCoreAndPatcherMods();
}
});
@@ -919,6 +962,69 @@ ipcMain.handle("add-offline-mod", async () => {
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 ////////////////////

View File

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