180 lines
5.4 KiB
TypeScript
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();
|
|
};
|