Files
Silk-Fly-Launcher/main.js

439 lines
11 KiB
JavaScript

const { app, BrowserWindow , ipcMain, dialog, shell} = require('electron/main');
const path = require('node:path');
const Store = require('electron-store').default;
const fs = require('fs/promises');
const { createWriteStream } = require('fs');
const { pipeline } = require('stream/promises');
const extract = require('extract-zip');
const Nexus = require('@nexusmods/nexus-api').default;
const store = new Store();
const userSavePath = app.getPath('userData')
const dataPath = `${userSavePath}\\config.json`
let silksongPath = store.get('silksong-path')
let nexusAPI = store.get('nexus-api')
let nexus = undefined
createNexus()
let bepinexFolderPath = `${silksongPath}/BepInEx`
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`
const bepinexFiles = [
".doorstop_version",
"changelog.txt",
"doorstop_config.ini",
"winhttp.dll"
]
let bepinexVersion
let bepinexBackupVersion
let mainWindow
let htmlFile
async function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
if(await fileExists(dataPath)) {
htmlFile = "index.html"
}
else {
htmlFile = "welcome.html"
}
mainWindow.loadFile(`renderer/${htmlFile}`)
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
///////////////// SAVING AND LOADING /////////////////
ipcMain.handle('save-path', (event, path) => {
saveSilksongPath(path)
});
function saveSilksongPath(path) {
silksongPath = path;
bepinexFolderPath = `${silksongPath}/BepInEx`
bepinexBackupPath = `${silksongPath}/BepInEx-Backup`
store.set('silksong-path', silksongPath);
}
ipcMain.handle('load-path', () => {
silksongPath = store.get('silksong-path');
if (silksongPath == undefined) {
return "";
}
return silksongPath;
});
function saveBepinexVersion(version) {
bepinexVersion = version;
if (bepinexVersion == undefined) {
store.delete('bepinex-version');
return;
}
store.set('bepinex-version', version);
};
ipcMain.handle('load-bepinex-version', () => {
bepinexVersion = store.get('bepinex-version');
return bepinexVersion;
});
function saveBepinexBackupVersion(version) {
bepinexBackupVersion = version;
if (bepinexBackupVersion == undefined) {
store.delete('bepinex-backup-version');
return;
}
store.set('bepinex-backup-version', version);
};
ipcMain.handle('load-bepinex-backup-version', () => {
bepinexBackupVersion = store.get('bepinex-backup-version');
return bepinexBackupVersion;
});
ipcMain.handle('save-nexus-api', (event, api) => {
nexusAPI = api;
createNexus()
store.set('nexus-api', nexusAPI);
});
ipcMain.handle('load-nexus-api', () => {
nexusAPI = store.get('nexus-api');
if (nexusAPI == undefined) {
return "";
}
return nexusAPI;
});
ipcMain.handle('save-theme', (event, theme) => {
store.set('theme', theme);
});
ipcMain.handle('load-theme', () => {
theme = store.get('theme');
if (theme == undefined) {
return "Silksong";
}
return theme;
});
//////////////////////////////////////////////////////
/////////////////// DATA HANDLING ////////////////////
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
ipcMain.handle('delete-data', async () => {
if (await fileExists(dataPath)) {
await fs.unlink(dataPath)
}
});
ipcMain.handle('export-data', async () => {
if (!await fileExists(dataPath)) {
return
}
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Export Data',
defaultPath: 'config.json',
filters: [
{ name: 'JSON', extensions: ['json'] }
]
})
if (canceled || !filePath) return
await fs.copyFile(dataPath, filePath)
})
ipcMain.handle('import-data', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: 'Import Data',
properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }]
})
if (canceled || !filePaths) return false
if(await fileExists(dataPath)) {
await fs.unlink(dataPath)
}
await fs.copyFile(filePaths[0], dataPath,fs.constants.COPYFILE_EXCL)
return true
})
//////////////////////////////////////////////////////
////////////////////// BEPINEX ///////////////////////
async function installBepinex() {
if (await fileExists(bepinexBackupPath)) {
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) {
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true })
}
for (const file of bepinexFiles) {
const filePath = `${silksongPath}/${file}`
if (await fileExists(`${bepinexBackupPath}/${file}`)) {
await fs.copyFile(`${bepinexBackupPath}/${file}`, filePath)
}
}
await fs.rm(bepinexBackupPath, { recursive: true })
bepinexBackupVersion = store.get('bepinex-backup-version')
saveBepinexVersion(bepinexBackupVersion)
saveBepinexBackupVersion(undefined)
}
else {
const GITHUB_URL = "https://api.github.com/repos/bepinex/bepinex/releases/latest"
const res = await fetch(GITHUB_URL, {
headers: {
"User-Agent": "SilkFlyLauncher/1.0.0",
"Accept": "application/vnd.github+json",
}
})
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`)
}
const release = await res.json();
const asset = release.assets.find(
a => a.name.endsWith(".zip") && a.name.toLowerCase().includes("win_x64")
);
const download = await fetch(asset.browser_download_url)
if (!download.ok) {
throw new Error("Download error");
}
const filePath = `${userSavePath}\\bepinex.zip`
await pipeline(
download.body,
createWriteStream(filePath)
)
await extract(filePath, { dir: silksongPath})
await fs.unlink(filePath)
saveBepinexVersion(release.tag_name)
}
}
ipcMain.handle('install-bepinex', async () => {
await installBepinex()
})
async function uninstallBepinex() {
if (await fileExists(bepinexFolderPath)) {
await fs.rm(bepinexFolderPath, { recursive: true })
}
for (const file of bepinexFiles) {
const filePath = `${silksongPath}/${file}`
if (await fileExists(filePath)) {
await fs.unlink(filePath)
}
}
saveBepinexVersion(undefined)
}
ipcMain.handle('uninstall-bepinex', async () => {
await uninstallBepinex()
})
async function backupBepinex() {
if (await fileExists(bepinexBackupPath) == false) {
await fs.mkdir(bepinexBackupPath)
}
if (await fileExists(bepinexFolderPath)) {
await fs.cp(bepinexFolderPath, `${bepinexBackupPath}/BepInEx`, { recursive: true })
}
for (const file of bepinexFiles) {
const filePath = `${silksongPath}/${file}`
if (await fileExists(filePath)) {
await fs.copyFile(filePath, `${bepinexBackupPath}/${file}`)
}
}
saveBepinexBackupVersion(bepinexVersion)
await uninstallBepinex()
}
ipcMain.handle('backup-bepinex', async () => {
await backupBepinex()
})
ipcMain.handle('delete-bepinex-backup', async () => {
if (await fileExists(bepinexBackupPath)) {
await fs.rm(bepinexBackupPath, { recursive: true })
saveBepinexBackupVersion(undefined)
}
})
//////////////////////////////////////////////////////
/////////////////////// NEXUS ////////////////////////
async function createNexus() {
if (nexusAPI == undefined) {
return
}
try {
nexus = await Nexus.create(
nexusAPI,
'silk-fly-launcher',
'1.0.0',
'hollowknightsilksong'
);
} catch (error) {
nexus = undefined
}
}
ipcMain.handle('verify-nexus-api', async () => {
return await verifyNexusAPI()
})
async function verifyNexusAPI() {
if (nexus == undefined) {
return false
}
if (await nexus.getValidationResult()) {
return true
}
}
ipcMain.handle('get-latest-mods', async () => {
if (nexus == undefined) {
return
}
mods = await nexus.getLatestAdded()
return mods
})
ipcMain.handle('download-mod', async (event, link) => {
if (nexus == undefined) {
return
}
const nexusWindow = new BrowserWindow({
width: 1080,
height: 720,
modal: true,
parent: mainWindow,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
})
nexusWindow.loadURL(link)
})
//////////////////////////////////////////////////////
//////////////////// UNCATEGORIZE ////////////////////
ipcMain.handle('auto-detect-game-path', async () => {
const defaultsSilksongPaths = [
":/Program Files (x86)/Steam/steamapps/common/Hollow Knight Silksong",
":/SteamLibrary/steamapps/common/Hollow Knight Silksong"
]
for (const path of defaultsSilksongPaths) {
for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
const fullPath = `${String.fromCharCode(i)}${path}`
if (await fileExists(fullPath)) {
saveSilksongPath(fullPath)
return
}
}
}
})
ipcMain.handle('load-main-page', () => {
htmlFile = "index.html"
mainWindow.loadFile(`renderer/${htmlFile}`)
})
ipcMain.handle('get-page', () => {
return htmlFile
})
ipcMain.handle('open-link', async (event, link) => {
await shell.openExternal(link)
})
ipcMain.handle('open-window', async (event, file) => {
const win = new BrowserWindow({
width: 600,
height: 720,
modal: true,
parent: mainWindow,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
})
win.title = file
win.loadFile(file)
})
ipcMain.handle('launch-game', async (event, mode) => {
const silksongExecutablePath = `${silksongPath}/Hollow Knight Silksong.exe`
if (mode === "modded"){
if (await fileExists(bepinexFolderPath)) {
await shell.openExternal(silksongExecutablePath)
}
else {
await installBepinex()
await shell.openExternal(silksongExecutablePath)
}
}
if (mode === "vanilla"){
if (await fileExists(bepinexFolderPath)) {
await backupBepinex()
await shell.openExternal(silksongExecutablePath)
}
else {
await shell.openExternal(silksongExecutablePath)
}
}
})