import * as fs from 'fs'; import * as path from 'path'; import { detectActiveWindow } from './active-window'; import { ENGINE_SIGNATURE_FILES, GameCandidateInput, MIN_GAME_CONFIDENCE, ScoredGameCandidate, scoreCandidate, shouldIgnoreProcess } from './heuristics'; import { listRunningProcessNames } from '../process-list'; import { readDesktopSettings } from '../desktop-settings'; /** * Public result of a detection scan. The renderer prefers `candidate` and only * falls back to `fallbackProcessNames` when no focused candidate clears the * minimum confidence threshold. The fallback list is intentionally trimmed and * pre-filtered so the renderer never sees obvious non-games like Spotify. */ export interface GameDetectionResult { candidate: ScoredGameCandidate | null; /** * Filtered list of plausible game process names. Empty when the focused * candidate already crossed the threshold (so the renderer skips fallback * matching). Capped to keep RAWG quota usage predictable. */ fallbackProcessNames: string[]; } const MAX_FALLBACK_PROCESSES = 8; export async function detectActiveGame(): Promise { const ignoredProcessNames = getUserIgnoredProcesses(); const active = await detectActiveWindow(); let candidate: ScoredGameCandidate | null = null; if (active) { const hasEngineSignature = await detectEngineSignature(active.executablePath); const input: GameCandidateInput = { processName: active.processName, rawProcessName: active.processName, executablePath: active.executablePath, windowTitle: active.windowTitle, pid: active.pid, bounds: active.bounds, isFullscreen: active.isFullscreen, source: 'foreground', ignoredProcessNames, hasEngineSignature }; candidate = scoreCandidate(input); } if (candidate && candidate.confidence >= MIN_GAME_CONFIDENCE) { return { candidate, fallbackProcessNames: [] }; } const fallbackProcessNames = await collectFallbackProcessNames(ignoredProcessNames); return { candidate, fallbackProcessNames }; } async function collectFallbackProcessNames(ignoredProcessNames: ReadonlySet): Promise { try { const names = await listRunningProcessNames(); const filtered: string[] = []; for (const name of names) { if (filtered.length >= MAX_FALLBACK_PROCESSES) { break; } if (!shouldIgnoreProcess(name, ignoredProcessNames)) { filtered.push(name); } } return filtered; } catch { return []; } } async function detectEngineSignature(executablePath: string | undefined): Promise { if (!executablePath) { return false; } try { const directory = path.dirname(executablePath); const entries = await fs.promises.readdir(directory).catch(() => []); const lowerEntries = new Set(entries.map((entry) => entry.toLowerCase())); if (ENGINE_SIGNATURE_FILES.some((file) => lowerEntries.has(file.toLowerCase()))) { return true; } // Unreal Engine ships executables ending in "-Win64-Shipping.exe" or // "-Linux-Shipping" inside /Binaries//. return entries.some((entry) => /-(win64|win32|linux)-shipping(\.exe)?$/i.test(entry)); } catch { return false; } } function getUserIgnoredProcesses(): ReadonlySet { try { const stored = readDesktopSettings().ignoredGameProcesses ?? []; return new Set(stored.map((entry) => entry.trim().toLowerCase()).filter(Boolean)); } catch { return new Set(); } } export type { ScoredGameCandidate } from './heuristics';