diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 0383c2d3..e66d04b6 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -362,6 +362,11 @@ export class DownloadManager { if (download.automaticallyExtract) { this.handleExtraction(download, game); + } else { + // For downloads without extraction (e.g., torrents with ready-to-play files), + // search for executable in the download folder + const gameFilesManager = new GameFilesManager(game.shop, game.objectId); + gameFilesManager.searchAndBindExecutable(); } await this.processNextQueuedDownload(); diff --git a/src/main/services/game-executables.ts b/src/main/services/game-executables.ts new file mode 100644 index 00000000..05bd60cf --- /dev/null +++ b/src/main/services/game-executables.ts @@ -0,0 +1,13 @@ +import { gameExecutables } from "./process-watcher"; + +export class GameExecutables { + static getExecutablesForGame(objectId: string): string[] | null { + const executables = gameExecutables[objectId]; + + if (!executables || executables.length === 0) { + return null; + } + + return executables.map((exe) => exe.exe); + } +} diff --git a/src/main/services/game-files-manager.ts b/src/main/services/game-files-manager.ts index f3684a0a..66044476 100644 --- a/src/main/services/game-files-manager.ts +++ b/src/main/services/game-files-manager.ts @@ -7,6 +7,7 @@ import { SevenZip, ExtractionProgress } from "./7zip"; import { WindowManager } from "./window-manager"; import { publishExtractionCompleteNotification } from "./notifications"; import { logger } from "./logger"; +import { GameExecutables } from "./game-executables"; const PROGRESS_THROTTLE_MS = 1000; @@ -151,6 +152,96 @@ export class GameFilesManager { if (publishNotification && game) { publishExtractionCompleteNotification(game); } + + await this.searchAndBindExecutable(); + } + + async searchAndBindExecutable(): Promise { + try { + const [download, game] = await Promise.all([ + downloadsSublevel.get(this.gameKey), + gamesSublevel.get(this.gameKey), + ]); + + if (!download || !game || game.executablePath) { + return; + } + + const executableNames = GameExecutables.getExecutablesForGame( + this.objectId + ); + + if (!executableNames || executableNames.length === 0) { + return; + } + + const gameFolderPath = path.join( + download.downloadPath, + download.folderName! + ); + + if (!fs.existsSync(gameFolderPath)) { + return; + } + + const foundExePath = await this.findExecutableInFolder( + gameFolderPath, + executableNames + ); + + if (foundExePath) { + logger.info( + `[GameFilesManager] Auto-detected executable for ${this.objectId}: ${foundExePath}` + ); + + await gamesSublevel.put(this.gameKey, { + ...game, + executablePath: foundExePath, + }); + + WindowManager.mainWindow?.webContents.send("on-library-batch-complete"); + } + } catch (err) { + logger.error( + `[GameFilesManager] Error searching for executable: ${this.objectId}`, + err + ); + } + } + + private async findExecutableInFolder( + folderPath: string, + executableNames: string[] + ): Promise { + const normalizedNames = new Set( + executableNames.map((name) => name.toLowerCase()) + ); + + try { + const entries = await fs.promises.readdir(folderPath, { + withFileTypes: true, + recursive: true, + }); + + for (const entry of entries) { + if (!entry.isFile()) continue; + + const fileName = entry.name.toLowerCase(); + + if (normalizedNames.has(fileName)) { + const parentPath = + "parentPath" in entry + ? entry.parentPath + : (entry as unknown as { path?: string }).path || folderPath; + + return path.join(parentPath, entry.name); + } + } + } catch { + // Silently fail if folder cannot be read + } + + return null; } async extractDownloadedFile() { diff --git a/src/main/services/index.ts b/src/main/services/index.ts index e6ceef03..cc37b8ae 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -10,6 +10,7 @@ export * from "./ludusavi"; export * from "./cloud-sync"; export * from "./7zip"; export * from "./game-files-manager"; +export * from "./game-executables"; export * from "./common-redist-manager"; export * from "./aria2"; export * from "./ws"; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index db5bbee1..ee19294d 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -69,7 +69,7 @@ const getGameExecutables = async () => { return gameExecutables; }; -const gameExecutables = await getGameExecutables(); +export const gameExecutables = await getGameExecutables(); const findGamePathByProcess = async ( processMap: Map>, diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 29feabf5..f0df0547 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -225,6 +225,16 @@ export function GameDetailsContextProvider({ }; }, [game?.id, isGameRunning, updateGame]); + useEffect(() => { + const unsubscribe = window.electron.onLibraryBatchComplete(() => { + updateGame(); + }); + + return () => { + unsubscribe(); + }; + }, [updateGame]); + useEffect(() => { const handler = (ev: Event) => { try {