Files
Myx a173299ad3
Some checks failed
Queue Release Build / prepare (push) Successful in 27s
Deploy Web Apps / deploy (push) Successful in 10m8s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
Queue Release Build / build-linux (push) Has been cancelled
fix: Game detection improvements
2026-05-17 17:47:40 +02:00

120 lines
3.5 KiB
TypeScript

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<GameDetectionResult> {
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<string>): Promise<string[]> {
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<boolean> {
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 <Game>/Binaries/<Platform>/.
return entries.some((entry) => /-(win64|win32|linux)-shipping(\.exe)?$/i.test(entry));
} catch {
return false;
}
}
function getUserIgnoredProcesses(): ReadonlySet<string> {
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';