Refactor path handling, BepInEx management and startup logic

This commit is contained in:
2026-02-24 18:13:10 +01:00
parent 8924e1f882
commit 53f1afcf66
2 changed files with 80 additions and 56 deletions

132
main.js
View File

@@ -17,26 +17,25 @@ const __dirname = path.dirname(__filename);
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
const isDev = !app.isPackaged; const isDev = !app.isPackaged;
const VERSION = packageJson.version; const VERSION = packageJson.version;
const NAME = packageJson.productName;
const userAgent = `${NAME}/${VERSION}`;
const store = new Store(); const store = new Store();
const bepinexStore = new Store({ name: "bepinex-version" }); const bepinexStore = new Store({ name: "bepinex-version" });
const installedModsStore = new Store({ name: "installed-mods-list" }); const installedModsStore = new Store({ name: "installed-mods-list" });
const NexusAPIStore = new Store({ name: "nexus-api", encryptionKey: packageJson["AES-key-nexus-api"], fileExtension: "encrypted", clearInvalidConfig: true });
const userSavePath = app.getPath("userData"); const userSavePath = app.getPath("userData");
const modSavePath = `${userSavePath}\\mods`; const modSavePath = path.join(userSavePath, "mods");
const dataPath = `${userSavePath}\\config.json`; const dataPath = path.join(userSavePath, "config.json");
let silksongPath = store.get("silksong-path"); let sevenZipPath = path7za;
const NexusAPIStore = new Store({ name: "nexus-api", encryptionKey: packageJson["AES-key-nexus-api"], fileExtension: "encrypted", clearInvalidConfig: true });
const Nexus = NexusModule.default; const Nexus = NexusModule.default;
let nexus = undefined; let nexus;
let installedCachedModList = undefined; let installedCachedModList;
let onlineCachedModList = undefined; let onlineCachedModList;
let bepinexFolderPath = `${silksongPath}/BepInEx`;
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll"]; const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll"];
let bepinexVersion; let bepinexVersion;
let bepinexBackupVersion; let bepinexBackupVersion;
@@ -47,12 +46,6 @@ let htmlFile;
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
////////////////////// STARTUP /////////////////////// ////////////////////// STARTUP ///////////////////////
let sevenZipPath = path7za;
if (!isDev) {
sevenZipPath = path7za.replace("\\app.asar\\node_modules", "");
Menu.setApplicationMenu(null);
}
if (!gotTheLock) { if (!gotTheLock) {
app.quit(); app.quit();
} else { } else {
@@ -71,6 +64,7 @@ async function createWindow() {
webPreferences: { webPreferences: {
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
}, },
show: false,
}); });
if (await fileExists(dataPath)) { if (await fileExists(dataPath)) {
@@ -79,7 +73,11 @@ async function createWindow() {
htmlFile = "welcome.html"; htmlFile = "welcome.html";
} }
mainWindow.loadFile(`renderer/${htmlFile}`); mainWindow.loadFile(path.join("renderer", htmlFile));
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
} }
app.whenReady().then(() => { app.whenReady().then(() => {
@@ -87,6 +85,8 @@ app.whenReady().then(() => {
app.setAsDefaultProtocolClient("nxm", process.execPath, [path.resolve(process.argv[1])]); app.setAsDefaultProtocolClient("nxm", process.execPath, [path.resolve(process.argv[1])]);
} else { } else {
app.setAsDefaultProtocolClient("nxm"); app.setAsDefaultProtocolClient("nxm");
sevenZipPath = path7za.replace("\\app.asar\\node_modules", "");
Menu.setApplicationMenu(null);
} }
if (gotTheLock) { if (gotTheLock) {
@@ -119,18 +119,19 @@ ipcMain.handle("save-path", (event, path) => {
saveSilksongPath(path); saveSilksongPath(path);
}); });
function saveSilksongPath(path) { function saveSilksongPath(path) {
silksongPath = path; store.set("silksong-path", path);
bepinexFolderPath = `${silksongPath}/BepInEx`;
bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
store.set("silksong-path", silksongPath);
} }
ipcMain.handle("load-path", () => { function loadSilksongPath() {
silksongPath = store.get("silksong-path"); const silksongPath = store.get("silksong-path");
if (silksongPath == undefined) { if (silksongPath == undefined) {
return ""; return "";
} }
return silksongPath; return silksongPath;
}
ipcMain.handle("load-path", () => {
return loadSilksongPath();
}); });
function saveBepinexVersion(version) { function saveBepinexVersion(version) {
@@ -262,33 +263,31 @@ ipcMain.handle("import-data", async () => {
////////////////////// BEPINEX /////////////////////// ////////////////////// BEPINEX ///////////////////////
async function installBepinex() { async function installBepinex() {
const silksongPath = loadSilksongPath();
const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
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");
return; return;
} }
if (await fileExists(bepinexBackupPath)) { if (await fileExists(bepinexBackupPath)) {
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) { mainWindow.webContents.send("showToast", "Installing Bepinex from Backup");
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true });
}
for (const file of bepinexFiles) { await fs.cp(bepinexBackupPath, silksongPath, { recursive: true });
const filePath = `${silksongPath}/${file}`;
if (await fileExists(`${bepinexBackupPath}/${file}`)) {
await fs.copyFile(`${bepinexBackupPath}/${file}`, filePath);
}
}
await fs.rm(bepinexBackupPath, { recursive: true }); await fs.rm(bepinexBackupPath, { recursive: true });
bepinexBackupVersion = bepinexStore.get("bepinex-backup-version"); bepinexBackupVersion = bepinexStore.get("bepinex-backup-version");
saveBepinexVersion(bepinexBackupVersion); saveBepinexVersion(bepinexBackupVersion);
saveBepinexBackupVersion(undefined); saveBepinexBackupVersion(undefined);
} else { } else {
mainWindow.webContents.send("showToast", "Installing Bepinex from Github");
const GITHUB_URL = "https://api.github.com/repos/bepinex/bepinex/releases/latest"; const GITHUB_URL = "https://api.github.com/repos/bepinex/bepinex/releases/latest";
const res = await fetch(GITHUB_URL, { const res = await fetch(GITHUB_URL, {
headers: { headers: {
"User-Agent": `SilkFlyLauncher/${VERSION}`, "User-Agent": userAgent,
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
}, },
}); });
@@ -310,7 +309,7 @@ async function installBepinex() {
} }
if (await fileExists(modSavePath)) { if (await fileExists(modSavePath)) {
await fs.cp(`${modSavePath}`, `${bepinexFolderPath}/plugins`, { recursive: true }); await fs.cp(modSavePath, path.join(silksongPath, "BepInEx", "plugins"), { recursive: true });
} }
} }
@@ -319,6 +318,9 @@ ipcMain.handle("install-bepinex", async () => {
}); });
async function uninstallBepinex() { async function uninstallBepinex() {
const silksongPath = loadSilksongPath();
const bepinexFolderPath = path.join(silksongPath, "BepInEx");
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");
return; return;
@@ -329,7 +331,7 @@ async function uninstallBepinex() {
} }
for (const file of bepinexFiles) { for (const file of bepinexFiles) {
const filePath = `${silksongPath}/${file}`; const filePath = path.join(silksongPath, file);
if (await fileExists(filePath)) { if (await fileExists(filePath)) {
await fs.unlink(filePath); await fs.unlink(filePath);
} }
@@ -342,6 +344,11 @@ ipcMain.handle("uninstall-bepinex", async () => {
}); });
async function backupBepinex() { async function backupBepinex() {
const silksongPath = loadSilksongPath();
const bepinexFolderPath = path.join(silksongPath, "BepInEx");
const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
const BepinexPluginsPath = path.join(silksongPath, "BepInEx", "plugins");
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");
return; return;
@@ -351,20 +358,20 @@ async function backupBepinex() {
await fs.mkdir(bepinexBackupPath); await fs.mkdir(bepinexBackupPath);
} }
if (fileExists(`${bepinexFolderPath}/plugins`)) { if (fileExists(BepinexPluginsPath)) {
await fs.rm(`${bepinexFolderPath}/plugins`, { recursive: true }); await fs.rm(BepinexPluginsPath, { recursive: true });
} }
if (await fileExists(bepinexFolderPath)) { if (await fileExists(bepinexFolderPath)) {
await fs.cp(bepinexFolderPath, `${bepinexBackupPath}/BepInEx`, { await fs.cp(bepinexFolderPath, path.join(bepinexBackupPath, "BepInEx"), {
recursive: true, recursive: true,
}); });
} }
for (const file of bepinexFiles) { for (const file of bepinexFiles) {
const filePath = `${silksongPath}/${file}`; const filePath = path.join(silksongPath, file);
if (await fileExists(filePath)) { if (await fileExists(filePath)) {
await fs.copyFile(filePath, `${bepinexBackupPath}/${file}`); await fs.copyFile(filePath, path.join(bepinexBackupPath, file));
} }
} }
@@ -377,6 +384,9 @@ ipcMain.handle("backup-bepinex", async () => {
}); });
ipcMain.handle("delete-bepinex-backup", async () => { ipcMain.handle("delete-bepinex-backup", async () => {
const silksongPath = loadSilksongPath();
const bepinexBackupPath = path.join(silksongPath, "BepInEx-Backup");
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");
return; return;
@@ -398,7 +408,7 @@ async function createNexus(api) {
} }
try { try {
nexus = await Nexus.create(api, "silk-fly-launcher", VERSION, "hollowknightsilksong"); nexus = await Nexus.create(api, NAME, VERSION, "hollowknightsilksong");
} catch (error) { } catch (error) {
if (error.mStatusCode == 401) { if (error.mStatusCode == 401) {
mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error"); mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error");
@@ -452,6 +462,7 @@ ipcMain.handle("open-download", async (event, link) => {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
}, },
backgroundColor: "#000000",
}); });
nexusWindow.loadURL(link); nexusWindow.loadURL(link);
@@ -473,6 +484,9 @@ function handleNxmUrl(url) {
} }
async function startDownload(modId, fileId, key, expires) { async function startDownload(modId, fileId, key, expires) {
modId = String(modId);
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
if (!(await verifyNexusAPI())) { if (!(await verifyNexusAPI())) {
mainWindow.webContents.send("showToast", "Unable to download.", "error"); mainWindow.webContents.send("showToast", "Unable to download.", "error");
return; return;
@@ -485,9 +499,9 @@ async function startDownload(modId, fileId, key, expires) {
await fs.mkdir(modSavePath); await fs.mkdir(modSavePath);
} }
await downloadAndUnzip(download_url, `${modSavePath}/${modId}`); await downloadAndUnzip(download_url, path.join(modSavePath, modId));
if (await fileExists(bepinexFolderPath)) { if (await fileExists(bepinexFolderPath)) {
await fs.cp(`${modSavePath}/${modId}`, `${bepinexFolderPath}/plugins/${modId}`, { recursive: true }); await fs.cp(path.join(modSavePath, modId), path.join(bepinexFolderPath, "plugins", modId), { recursive: true });
} }
saveModInfo(modId); saveModInfo(modId);
@@ -496,18 +510,23 @@ async function startDownload(modId, fileId, key, expires) {
} }
async function checkInstalledMods() { async function checkInstalledMods() {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
for (const [key, modInfo] of Object.entries(installedModsStore.store)) { for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
if (!(await fileExists(`${modSavePath}/${modInfo.modId}`))) { modInfo.modId = String(modInfo.modId);
if (!(await fileExists(path.join(modSavePath, modInfo.modId)))) {
saveModInfo(key, true); saveModInfo(key, true);
await fs.rm(`${bepinexFolderPath}/plugins/${modInfo.modId}`, { recursive: true }); await fs.rm(path.join(bepinexFolderPath, "plugins", modInfo.modId), { recursive: true });
} }
} }
} }
ipcMain.handle("uninstall-mod", async (event, modId) => { ipcMain.handle("uninstall-mod", async (event, modId) => {
const modPath = `${bepinexFolderPath}/plugins/${modId}`; modId = String(modId);
if (await fileExists(`${modSavePath}/${modId}`)) { const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
await fs.rm(`${modSavePath}/${modId}`, { recursive: true }); const modPath = path.join(BepinexPluginsPath, modId);
if (await fileExists(path.join(modSavePath, modId))) {
await fs.rm(path.join(modSavePath, modId), { recursive: true });
} }
if (await fileExists(modPath)) { if (await fileExists(modPath)) {
await fs.rm(modPath, { recursive: true }); await fs.rm(modPath, { recursive: true });
@@ -535,7 +554,7 @@ async function searchNexusMods(keywords, offset = 0, count = 10, sortFilter = "d
const endpoint = "https://api.nexusmods.com/v2/graphql"; const endpoint = "https://api.nexusmods.com/v2/graphql";
const client = new GraphQLClient(endpoint, { const client = new GraphQLClient(endpoint, {
headers: { headers: {
"User-Agent": `SilkFlyLauncher/${VERSION}`, "User-Agent": userAgent,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
@@ -631,7 +650,7 @@ ipcMain.handle("auto-detect-game-path", async () => {
ipcMain.handle("load-main-page", () => { ipcMain.handle("load-main-page", () => {
htmlFile = "index.html"; htmlFile = "index.html";
mainWindow.loadFile(`renderer/${htmlFile}`); mainWindow.loadFile(path.join("renderer", htmlFile));
}); });
ipcMain.handle("get-page", () => { ipcMain.handle("get-page", () => {
@@ -659,7 +678,12 @@ ipcMain.handle("open-window", async (event, file) => {
}); });
ipcMain.handle("launch-game", async (event, mode) => { ipcMain.handle("launch-game", async (event, mode) => {
const silksongExecutablePath = `${silksongPath}/Hollow Knight Silksong.exe`; const silksongExecutablePath = path.join(loadSilksongPath(), "Hollow Knight Silksong.exe");
if (!fileExists(silksongExecutablePath)) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if (mode === "modded") { if (mode === "modded") {
if (await fileExists(bepinexFolderPath)) { if (await fileExists(bepinexFolderPath)) {
await shell.openExternal(silksongExecutablePath); await shell.openExternal(silksongExecutablePath);
@@ -678,7 +702,7 @@ ipcMain.handle("launch-game", async (event, mode) => {
} }
}); });
async function downloadAndUnzip(url, path) { async function downloadAndUnzip(url, toPath) {
url = new URL(url); url = new URL(url);
const fileName = url.pathname.split("/").pop(); const fileName = url.pathname.split("/").pop();
const extension = fileName.split(".").pop().toLowerCase(); const extension = fileName.split(".").pop().toLowerCase();
@@ -689,9 +713,9 @@ async function downloadAndUnzip(url, path) {
return; return;
} }
const tempPath = `${userSavePath}\\tempZip.${extension}`; const tempPath = path.join(userSavePath, `tempArchive.${extension}`);
await pipeline(download.body, createWriteStream(tempPath)); await pipeline(download.body, createWriteStream(tempPath));
await extractArchive(tempPath, path); await extractArchive(tempPath, toPath);
await fs.unlink(tempPath); await fs.unlink(tempPath);
} }

View File

@@ -141,8 +141,8 @@ async function navigate(page) {
electronAPI.openExternalLink(modLink); electronAPI.openExternalLink(modLink);
}); });
modDownloadButton = installedModTemplateCopy.getElementById("uninstall-mod-button"); const uninstallModButton = installedModTemplateCopy.getElementById("uninstall-mod-button");
modDownloadButton.addEventListener("click", async function (event) { uninstallModButton.addEventListener("click", async function (event) {
event.preventDefault(); event.preventDefault();
await nexus.uninstall(modInfo.modId); await nexus.uninstall(modInfo.modId);