// BetterCurseForge — Main Process Hooks // ====================================== // Runs in Electron's main process BEFORE CurseForge loads. // Blocks ad network requests via session.webRequest. // Manages themes, plugins, and settings IPC channels. import { app, session, ipcMain, shell } from 'electron'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const USER_DATA = join(app.getPath('userData'), 'BetterCurseForge'); const THEMES_DIR = join(USER_DATA, 'themes'); const PLUGINS_DIR = join(USER_DATA, 'plugins'); const SETTINGS_FILE = join(USER_DATA, 'settings.json'); // Ensure user directories exist [THEMES_DIR, PLUGINS_DIR].forEach(d => { try { mkdirSync(d, { recursive: true }); } catch {} }); // Seed built-in themes and plugins if user directories are empty try { const builtinThemes = join(__dirname, '..', 'themes'); if (existsSync(builtinThemes)) { for (const f of readdirSync(builtinThemes)) { try { const src = join(builtinThemes, f); const dst = join(THEMES_DIR, f); if (!existsSync(dst)) { writeFileSync(dst, readFileSync(src)); } } catch {} } } } catch {} // ── Settings ────────────────────────────────────────────────── let settings = { adsBlocked: true, enabledThemes: [], enabledPlugins: [], activeCustomTheme: null, }; try { if (existsSync(SETTINGS_FILE)) { Object.assign(settings, JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8'))); } } catch {} function saveSettings() { try { writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); } catch {} } // ── Ad Blocking (Network Layer) ─────────────────────────────── const AD_URL_PATTERNS = [ '*://*.overwolf.com/ads/*', '*://ads.overwolf.com/*', '*://content.overwolf.com/ads/*', '*://*.adrta.com/*', '*://*.adnxs.com/*', '*://*.doubleclick.net/*', '*://*.googlesyndication.com/*', '*://*.googleadservices.com/*', '*://*.moatads.com/*', '*://*.amazon-adsystem.com/*', '*://*.adsrvr.org/*', '*://*.adscorebr.com/*', '*://*.serving-sys.com/*', '*://*.adskeeper.co.uk/*', ]; app.whenReady().then(() => { if (!settings.adsBlocked) return; try { const ses = session.defaultSession; ses.webRequest.onBeforeRequest({ urls: AD_URL_PATTERNS }, (details, callback) => { callback({ cancel: true }); }); console.log('[BetterCF] Network ad blocking enabled.'); } catch (err) { console.error('[BetterCF] Failed to set up ad blocking:', err); } }); // ── Renderer Injection via app event ────────────────────────── const rendererCode = (() => { const rendererPath = join(__dirname, 'renderer.js'); if (existsSync(rendererPath)) { return readFileSync(rendererPath, 'utf-8'); } return null; })(); app.on('browser-window-created', (_event, window) => { if (!rendererCode) return; window.webContents.on('did-finish-load', () => { // Inject the IPC bridge first (since contextIsolation is false, this works) const ipcBridge = ` if (!window.__BCF_IPC__) { try { const { ipcRenderer } = require('electron'); window.__BCF_IPC__ = { invoke: (...args) => ipcRenderer.invoke(...args), }; } catch (e) {} } window.__BCF_CORE_PATH__ = ${JSON.stringify(__dirname)}; `; window.webContents.executeJavaScript(ipcBridge).then(() => { return window.webContents.executeJavaScript(rendererCode); }).then(() => { console.log('[BetterCF] Renderer injected into window.'); }).catch(err => { console.error('[BetterCF] Renderer injection failed:', err); }); }); }); // ── IPC Channels ────────────────────────────────────────────── ipcMain.handle('bcf:themes:list', () => { try { if (!existsSync(THEMES_DIR)) return []; const entries = readdirSync(THEMES_DIR, { withFileTypes: true }); const themes = []; for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.css')) { const name = entry.name.replace('.css', ''); const css = readFileSync(join(THEMES_DIR, entry.name), 'utf-8'); const manifestPath = join(THEMES_DIR, `${name}.json`); let manifest = { name: name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), author: 'Custom', description: '', version: '1.0.0', primaryColor: '#8b5cf6', titleColor: '#a78bfa', bodyColor: '#2e1065', backgroundColor: '#161026', }; if (existsSync(manifestPath)) { try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {} } themes.push({ ...manifest, fileName: entry.name, css, enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name, }); } else if (entry.isDirectory()) { const cssPath = join(THEMES_DIR, entry.name, 'theme.css'); const manifestPath = join(THEMES_DIR, entry.name, 'theme.json'); if (existsSync(cssPath)) { const css = readFileSync(cssPath, 'utf-8'); let manifest = { name: entry.name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), author: 'Custom', description: '', version: '1.0.0', primaryColor: '#8b5cf6', titleColor: '#a78bfa', bodyColor: '#2e1065', backgroundColor: '#161026', }; if (existsSync(manifestPath)) { try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {} } themes.push({ ...manifest, fileName: entry.name, css, enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name, }); } } } return themes; } catch { return []; } }); ipcMain.handle('bcf:plugins:list', () => { try { if (!existsSync(PLUGINS_DIR)) return []; const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true }); const plugins = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const manifestPath = join(PLUGINS_DIR, entry.name, 'manifest.json'); if (!existsSync(manifestPath)) continue; try { const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); const indexPath = join(PLUGINS_DIR, entry.name, 'index.js'); const stylePath = join(PLUGINS_DIR, entry.name, 'styles.css'); plugins.push({ ...manifest, dirName: entry.name, hasScript: existsSync(indexPath), hasStyles: existsSync(stylePath), enabled: settings.enabledPlugins.includes(entry.name), script: existsSync(indexPath) ? readFileSync(indexPath, 'utf-8') : null, styles: existsSync(stylePath) ? readFileSync(stylePath, 'utf-8') : null, }); } catch {} } return plugins; } catch { return []; } }); ipcMain.handle('bcf:settings:get', () => ({ ...settings })); ipcMain.handle('bcf:settings:set', (_, key, value) => { settings[key] = value; saveSettings(); return true; }); ipcMain.handle('bcf:settings:toggle-theme', (_, fileName) => { const idx = settings.enabledThemes.indexOf(fileName); if (idx >= 0) settings.enabledThemes.splice(idx, 1); else settings.enabledThemes.push(fileName); saveSettings(); return settings.enabledThemes.includes(fileName); }); ipcMain.handle('bcf:settings:toggle-plugin', (_, dirName) => { const idx = settings.enabledPlugins.indexOf(dirName); if (idx >= 0) settings.enabledPlugins.splice(idx, 1); else settings.enabledPlugins.push(dirName); saveSettings(); return settings.enabledPlugins.includes(dirName); }); ipcMain.handle('bcf:open-themes-folder', async () => { try { await shell.openPath(THEMES_DIR); return true; } catch { return false; } }); ipcMain.handle('bcf:open-plugins-folder', async () => { try { await shell.openPath(PLUGINS_DIR); return true; } catch { return false; } }); ipcMain.handle('bcf:info', () => ({ version: '1.0.0', bcfRoot: USER_DATA, themesDir: THEMES_DIR, pluginsDir: PLUGINS_DIR, settingsFile: SETTINGS_FILE, })); console.log('[BetterCF] Main process hooks installed.'); // ── Auto-Repatch on Update Interceptor ──────────────────────── try { const { spawn } = await import('node:child_process'); const patcherExe = join(process.env.LOCALAPPDATA || '', 'BetterCurseForge', 'BetterCurseForge.exe'); const onUpdateTriggered = () => { try { if (existsSync(patcherExe)) { spawn(patcherExe, ['--wait-for-update', process.pid.toString()], { detached: true, stdio: 'ignore' }).unref(); console.log('[BetterCF] Update pending. Spawned patcher --wait-for-update'); } } catch (e) { console.error('[BetterCF] Failed to spawn update patcher:', e); } }; // 1. Check if curseforge-updater has a pending update when quitting app.on('before-quit', () => { const pendingInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'pending', 'setup.exe'); const directInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'installer.exe'); if (existsSync(pendingInstaller) || existsSync(directInstaller)) { onUpdateTriggered(); } }); // 2. Ensure watcher daemon is running setTimeout(() => { try { if (existsSync(patcherExe)) { spawn(patcherExe, ['--ensure-watcher'], { detached: true, stdio: 'ignore' }).unref(); } } catch {} }, 4000); } catch (err) { console.error('[BetterCF] Auto-repatch hook initialization error:', err); }