'use strict' const { runGame, runTool } = require('../runner') const { importGame } = require('../importer') const library = require('../library') function killProcessGroup(pid) { try { process.kill(-pid, 'SIGKILL') } catch {} try { process.kill(pid, 'SIGKILL') } catch {} } function sendToWindow(win, channel, data) { if (win && !win.isDestroyed()) win.webContents.send(channel, data) } // Game-process state: id -> pid (or null while the process is starting). // onGamesChanged fires in the MAIN process whenever the running-games set // changes (the tray menu rebuilds itself from it). function createGamesService({ getWindow, onGamesChanged }) { const running = new Map() const killRequests = new Set() let onGameExit = null const notifyChanged = () => { if (typeof onGamesChanged === 'function') onGamesChanged() } const isRunning = id => running.has(id) const listRunning = () => [...running.keys()] function resolveWine(game) { const system = { id: 'system', type: 'system', label: 'System Wine' } const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system') if (!wanted || wanted === 'system') return system return require('../wines').getWine(wanted) || system } function handleExit(id) { running.delete(id) sendToWindow(getWindow(), 'games:changed') notifyChanged() if (onGameExit) onGameExit(id) } async function start(win, id) { const game = library.getGame(id) if (!game) return { ok: false, error: 'game-not-found' } if (running.has(id)) return { ok: false, error: 'already-running' } running.set(id, null) let wine try { wine = resolveWine(game) } catch { running.delete(id) return { ok: false, error: 'wine-not-found' } } try { const res = await runGame({ game, wine, systemWine: wine.type === 'system', onExit: () => handleExit(id) }) if (res.pid) running.set(id, res.pid) // stop was requested while the process was still starting if (killRequests.has(id)) { killRequests.delete(id) if (res.pid) killProcessGroup(res.pid) } sendToWindow(win, 'games:changed') notifyChanged() return { ok: true, pid: res.pid || null, logFile: res.logFile || null } } catch (err) { running.delete(id) return { ok: false, error: String((err && err.message) || err) } } } async function kill(id) { const pid = running.get(id) if (pid) { killProcessGroup(pid) running.delete(id) sendToWindow(getWindow(), 'games:changed') notifyChanged() } else if (running.has(id)) { killRequests.add(id) } return { ok: true } } async function runToolFor(id, tool, args) { const game = library.getGame(id) if (!game) return { ok: false, error: 'game-not-found' } let wine try { wine = resolveWine(game) } catch { return { ok: false, error: 'wine-not-found' } } try { await runTool({ game, wine, tool, args, systemWine: wine.type === 'system' }) return { ok: true } } catch (err) { return { ok: false, error: String((err && err.message) || err) } } } // subscribe to game exits (used by the tray/headless mode to e.g. quit when // the game that auto-started the launcher has finished) function setOnGameExit(fn) { onGameExit = typeof fn === 'function' ? fn : null } async function importTo(win, srcDir, mode) { return importGame(srcDir, mode, data => sendToWindow(win, 'import:progress', data)) } return { isRunning, listRunning, start, kill, runToolFor, import: importTo, setOnGameExit } } module.exports = { createGamesService, killProcessGroup, sendToWindow }