fix: Game detection improvements
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
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
This commit is contained in:
119
electron/game-detection/index.ts
Normal file
119
electron/game-detection/index.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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';
|
||||
Reference in New Issue
Block a user