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
120 lines
3.5 KiB
TypeScript
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';
|