import { join } from 'node:path'; import { app, BrowserWindow, ipcMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron'; import { readFileSync } from 'node:fs'; import type { HostResult, MapFiles } from '../preload/api'; import { DISCOVERY_INTERVAL_MS } from '../shared/constants'; import { readMapFiles, readMapImage, resolveAssetsDir } from '../server/assets'; import { DiscoveryListener } from './discovery'; const assetsDir = app.isPackaged ? join(process.resourcesPath, 'assets') : join(app.getAppPath(), 'assets'); process.env.GOUTER_ASSETS = assetsDir; let server: UtilityProcess | null = null; let serverReady: Promise | null = null; /** Forks the authoritative server in a utilityProcess (once) and waits for it to listen. */ function hostServer(name: string): Promise { if (serverReady) return serverReady; serverReady = new Promise((resolveReady, reject) => { const proc = utilityProcess.fork(join(__dirname, 'server.js'), [`--name=${String(name).slice(0, 40)}`], { serviceName: 'goutermageddon-server', env: { ...process.env, GOUTER_ASSETS: assetsDir }, stdio: 'inherit', }); server = proc; proc.on('message', (msg: { type: string; port?: number; message?: string }) => { if (msg.type === 'ready') resolveReady({ port: msg.port! }); else if (msg.type === 'error') reject(new Error(msg.message)); }); proc.on('exit', (code) => { server = null; serverReady = null; reject(new Error(`server exited with code ${code}`)); }); }); return serverReady; } function createWindow(): void { const win = new BrowserWindow({ width: 1280, height: 720, title: 'Goƻtermageddon', backgroundColor: '#fff1d6', autoHideMenuBar: true, webPreferences: { preload: join(__dirname, '../preload/index.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); if (process.env.ELECTRON_RENDERER_URL) void win.loadURL(process.env.ELECTRON_RENDERER_URL); else void win.loadFile(join(__dirname, '../renderer/index.html')); } // LAN discovery: one listener for the app, pushed to the renderer that asked for it. const discovery = new DiscoveryListener(); let discoveryTimer: NodeJS.Timeout | null = null; function startDiscovery(target: WebContents): void { stopDiscovery(); discovery.start((err) => console.error('[discovery] cannot listen:', err.message)); const push = (): void => { if (target.isDestroyed()) return stopDiscovery(); target.send('lan:games', discovery.list()); }; push(); discoveryTimer = setInterval(push, DISCOVERY_INTERVAL_MS); } function stopDiscovery(): void { if (discoveryTimer) clearInterval(discoveryTimer); discoveryTimer = null; discovery.stop(); } ipcMain.handle('lan:host', (_event, name: string) => hostServer(name)); ipcMain.handle('lan:discover-start', (event) => startDiscovery(event.sender)); ipcMain.handle('lan:discover-stop', () => stopDiscovery()); ipcMain.handle('assets:materials', () => JSON.parse(readFileSync(join(resolveAssetsDir(), 'materials', 'materials.json'), 'utf8'))); ipcMain.handle('assets:map', (_event, mapId: string): MapFiles => { const dir = resolveAssetsDir(); const { definition, terrain } = readMapFiles(dir, mapId); return { definition, terrain, background: readMapImage(dir, mapId, 'background.png') }; }); void app.whenReady().then(() => { createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { stopDiscovery(); server?.kill(); app.quit(); });