Files
Docker Build f88a303327 feat(launcher): Linux play wrapper, patch UX, Gitea sync cleanup
- Play on Linux: use launch.linux_wrapper (wine) or linux_steam_uri; chmod .exe after install
- Windows: retry EBUSY on MPQ replace; download to .new before rename
- After successful sync: remove .bak-* backups; realmlist only Data/enUS (ignore enGB)
- Gitea/distro merge: skip Fractured-Launcher* from GitHub assets (CI default-branch build wins)
- Omit blockmap and builder-debug from staged artifacts and Gitea uploads; upload script validates before clearing attachments
- README and launcher version 1.0.12

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:20:22 -05:00

194 lines
6.5 KiB
JavaScript

'use strict';
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store');
const { normalizeWinGameDir } = require('./lib/win-game-dir');
const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch');
const { readPatchState } = require('./lib/patch-manifest');
const { setupAutoUpdater } = require('./lib/auto-update');
let mainWindow;
let autoUpdateApi = {
checkNow: async () => ({ skipped: true, reason: 'not initialized' }),
};
function createWindow() {
mainWindow = new BrowserWindow({
width: 720,
height: 640,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
},
});
Menu.setApplicationMenu(null);
mainWindow.loadFile(path.join(__dirname, 'index.html'));
mainWindow.once('ready-to-show', () => mainWindow.show());
}
function sendProgress(msg) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('launcher:progress', msg);
}
}
async function readMergedConfig() {
const { configPath, config } = await loadConfig(app);
const gameDir = resolveGameDir(config, configPath);
const merged = { ...config, game_dir: gameDir };
return { configPath, config: merged };
}
app.whenReady().then(async () => {
createWindow();
const { config } = await loadConfig(app);
const ghEnv = config.github && config.github.token_env;
const githubToken =
(ghEnv && String(process.env[ghEnv] || '').trim()) ||
String(process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '').trim();
const giteaEnv = config.gitea && config.gitea.token_env;
const giteaToken =
(giteaEnv && String(process.env[giteaEnv] || '').trim()) ||
String(process.env.GITEA_TOKEN || '').trim();
const updateFeedUrl = String(process.env.LAUNCHER_UPDATE_URL || config.update_feed_url || '').trim();
autoUpdateApi = await setupAutoUpdater(app, () => mainWindow, {
updateFeedUrl,
config,
githubOwner: config.github && config.github.owner,
githubRepo: config.github && config.github.repo,
githubToken,
giteaToken,
allowGithubLauncherUpdates: config.launcher_updates_from_github === true,
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
ipcMain.handle('launcher:load', async () => {
const { configPath, config } = await readMergedConfig();
let clientBuild = '';
if (wowInstallValid(config)) {
const st = await readPatchState(config.game_dir);
if (st && st.client_build) clientBuild = String(st.client_build);
}
return {
configPath,
gameDir: config.game_dir || '',
authEnabled: !!(config.auth && config.auth.enabled),
wowExe: (config.launch && config.launch.exe) || 'Wow.exe',
wowOk: wowInstallValid(config),
clientBuild,
};
});
ipcMain.handle('launcher:saveGameDir', async (_e, dir) => {
const trimmed = String(dir || '').trim();
if (!trimmed) throw new Error('folder path is empty');
const { configPath } = await loadConfig(app);
const norm =
process.platform === 'win32' ? normalizeWinGameDir(path.normalize(trimmed)) : path.normalize(trimmed);
const probe = { ...(await readMergedConfig()).config, game_dir: norm };
if (!wowInstallValid(probe)) {
throw new Error(`That folder does not contain ${(probe.launch && probe.launch.exe) || 'Wow.exe'}`);
}
const c = await saveGameDir(configPath, norm);
const merged = { ...c, game_dir: resolveGameDir(c, configPath) };
return { ok: true, gameDir: merged.game_dir, wowOk: wowInstallValid(merged) };
});
ipcMain.handle('launcher:pickFolder', async (_e, startDir) => {
const win = BrowserWindow.getFocusedWindow() || mainWindow;
const r = await dialog.showOpenDialog(win, {
title: 'Select World of Warcraft 3.3.5a folder',
properties: ['openDirectory', 'createDirectory'],
defaultPath: startDir && String(startDir).trim() ? String(startDir).trim() : undefined,
});
if (r.canceled || !r.filePaths || !r.filePaths[0]) return { canceled: true, path: '' };
return { canceled: false, path: r.filePaths[0] };
});
ipcMain.handle('launcher:auth', async (_e, { user, pass }) => {
const { config } = await readMergedConfig();
await doAuth(config, user, pass);
return { ok: true };
});
ipcMain.handle('launcher:sync', async () => {
const { config } = await readMergedConfig();
if (!wowInstallValid(config)) {
throw new Error('Set a valid WoW folder (must contain Wow.exe) first.');
}
await applyPatches(config, sendProgress);
return { ok: true };
});
ipcMain.handle('launcher:checkUpdates', async () => {
try {
return await autoUpdateApi.checkNow();
} catch (e) {
const msg = e && (e.message || String(e));
return { ok: false, error: msg };
}
});
ipcMain.handle('launcher:play', async () => {
const { config } = await readMergedConfig();
const exe = wowExePath(config);
const gameArgs = (config.launch && config.launch.args) || [];
const lc = config.launch || {};
const cwd = config.game_dir;
let cmd;
let spawnArgs;
if (process.platform === 'linux') {
const steamUri = String(lc.linux_steam_uri || '').trim();
const steamSpawn = Array.isArray(lc.linux_steam_spawn) ? lc.linux_steam_spawn.filter(Boolean) : [];
if (steamUri) {
if (steamSpawn.length) {
cmd = steamSpawn[0];
spawnArgs = [...steamSpawn.slice(1), steamUri];
} else {
const bin = String(lc.linux_steam_binary || 'steam').trim() || 'steam';
cmd = bin;
spawnArgs = [steamUri];
}
} else {
const wrap = Array.isArray(lc.linux_wrapper) ? lc.linux_wrapper.filter(Boolean) : [];
if (wrap.length) {
cmd = wrap[0];
spawnArgs = [...wrap.slice(1), exe, ...gameArgs];
} else {
throw new Error(
'On Linux, Wow.exe is a Windows program and cannot be run directly. ' +
'Set launch.linux_steam_uri (e.g. steam://rungameid/… for your Steam shortcut) ' +
'or launch.linux_wrapper (e.g. ["wine"]) in launcher.json.'
);
}
}
} else {
cmd = exe;
spawnArgs = gameArgs;
}
const child = spawn(cmd, spawnArgs, {
cwd,
detached: true,
stdio: 'ignore',
windowsHide: true,
shell: false,
});
child.unref();
return { ok: true };
});