import path from 'path'; import os from 'os'; import fs from 'fs-extra'; import Logger from 'electron-log/main'; import Preferences from '~main/modules/preferences'; import Mods from '~main/modules/mods'; import { mainWindow } from '~main/index'; import { isGameRunning } from '~main/modules/updater'; import { patchConfig } from '~main/modules/patcher'; import { applyLocalePatch } from '~main/modules/localePatch'; import { minimizeToTray, restoreFromTray } from '~main/modules/tray'; import { getMod } from '~common/mods'; import { ensureWowDesktopEntry, gameLaunchEnv, spawnDetachedGame, spawnWithProton } from '~main/modules/proton'; import { createTRPCRouter, publicProcedure } from '../trpc'; const chainloaderNeeded = async (clientDir: string): Promise => { const installed = Mods.status.mods.filter(r => r.installedVersion); if (installed.some(r => r.id === 'vanillaFixes')) return true; if (installed.some(r => getMod(r.id)?.requires?.includes('vanillaFixes'))) return true; const dllsPath = path.join(clientDir, 'dlls.txt'); if (await fs.pathExists(dllsPath)) { const raw = await fs.readFile(dllsPath, 'utf8'); return raw.split(/\r?\n/).some(l => l.trim() && !l.trim().startsWith('#')); } return false; }; type StartResult = { ok: boolean; error?: string }; export const launcherRouter = createTRPCRouter({ start: publicProcedure.mutation(async (): Promise => { const { cleanWdb, minimizeToTrayOnPlay, clientDir, useProton, protonPath } = Preferences.data; if (!clientDir) return { ok: false, error: 'No game folder is set.' }; const exePath = path.join(clientDir, 'WoW.exe'); if (!(await fs.pathExists(exePath))) return { ok: false, error: 'WoW.exe was not found in the game folder.' }; if (await isGameRunning(exePath)) return { ok: false, error: 'WoW is already running.' }; if (cleanWdb) { Logger.log('Cleaning up WDB...'); await fs.remove(path.join(clientDir, 'WDB')); } Logger.log('Checking Config.wtf...'); await patchConfig(); Logger.log('Applying UI language...'); await applyLocalePatch(clientDir, Preferences.data.locale); const loaderPath = path.join(clientDir, 'VanillaFixes.exe'); const needsLoader = await chainloaderNeeded(clientDir); const useLoader = needsLoader && (await fs.pathExists(loaderPath)); if (needsLoader && !useLoader) Logger.warn( 'VanillaFixes.exe is missing but mods/dlls.txt expect a chainloader; ' + 'launching WoW.exe directly (mods will not load).' ); const octoLocale = Preferences.data.locale || 'enUS'; const gameEnv = { ...process.env, OCTO_LOCALE: octoLocale }; const launchExe = useLoader ? loaderPath : exePath; const launchArgs = useLoader ? ['WoW.exe'] : []; const wantProton = os.platform() === 'linux' && !!useProton; if (wantProton && !protonPath) return { ok: false, error: 'Proton is enabled but no Proton version is selected.' }; Logger.log( wantProton ? `Launching via Proton${useLoader ? ' + VanillaFixes' : ''} (OCTO_LOCALE=${octoLocale})...` : useLoader ? `Launching via VanillaFixes (OCTO_LOCALE=${octoLocale})...` : `Launching ${exePath} (OCTO_LOCALE=${octoLocale})...` ); const launchEnv = gameLaunchEnv(gameEnv); if (os.platform() === 'linux') await ensureWowDesktopEntry(clientDir).catch(e => Logger.warn('Failed to write WoW desktop entry', e) ); let child; try { child = wantProton ? await spawnWithProton({ protonDir: protonPath!, exePath: launchExe, args: launchArgs, cwd: clientDir, env: launchEnv }) : spawnDetachedGame(launchExe, launchArgs, { env: launchEnv, cwd: clientDir }); } catch (e) { Logger.error('Failed to launch the game', e); const message = e instanceof Error ? e.message : String(e); return { ok: false, error: `Failed to launch the game: ${message}` }; } try { await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); }); } catch (e) { Logger.error('Failed to launch the game', e); const message = e instanceof Error ? e.message : String(e); return { ok: false, error: `Failed to launch the game: ${message}` }; } child.on('error', e => Logger.error('Game process error', e)); // VanillaFixes / Proton often exit right after spawning WoW — don't treat // that as the game ending. Tray restore uses isGameRunning polling. child.on('exit', code => { Logger.log( `Launch helper exited (code=${code}); tracking WoW.exe separately` ); }); if (!minimizeToTrayOnPlay) { mainWindow?.close(); return { ok: true }; } minimizeToTray(); void watchUntilGameExits(exePath); return { ok: true }; }) }); const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); /** Wait for WoW.exe to appear (chainloader/proton lag), then restore tray on exit. */ const watchUntilGameExits = async (exePath: string) => { const appearDeadline = Date.now() + 45_000; let seen = false; while (Date.now() < appearDeadline) { if (await isGameRunning(exePath)) { seen = true; break; } await sleep(500); } if (!seen) { Logger.warn('WoW.exe never appeared after launch; restoring tray'); restoreFromTray(); return; } Logger.log('WoW.exe detected; waiting for it to exit...'); while (await isGameRunning(exePath)) { await sleep(2000); } Logger.log('WoW stopped'); restoreFromTray(); };