Files
OctoLauncher/src/main/api/routers/launcher.ts
T
octocontr 275166559a
Build check / build-windows (pull_request) Has been cancelled
Build check / build-linux (pull_request) Has been cancelled
Linux build with proton picker
2026-07-29 23:27:59 -04:00

180 lines
5.4 KiB
TypeScript

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<boolean> => {
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<StartResult> => {
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<void>((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<void>(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();
};