From d7b3a9630e4c77ec538f128a8ba096c05c1e6efe Mon Sep 17 00:00:00 2001 From: Mathias Eek <51080320+Cliffback@users.noreply.github.com> Date: Tue, 19 May 2026 18:44:24 +0200 Subject: [PATCH 1/3] feat: add non-Steam games section for shortcuts added via Steam Parse Steam's binary shortcuts.vdf to list non-Steam game shortcuts and allow installing/uninstalling ReShade for them. Backend: - Add binary VDF parser for shortcuts.vdf - Add list_non_steam_games() to enumerate shortcuts - Add find_non_steam_game_executable_path() with scoring and script/flatpak shortcut handling - Add install/uninstall wrappers delegating to Heroic logic Frontend: - Add NonSteamGamesSection.tsx with game dropdown, executable detection, DLL override selector, and install/uninstall buttons - Register in index.tsx between Steam and Heroic sections Closes #29 --- main.py | 314 ++++++++++++++++++ src/NonSteamGamesSection.tsx | 596 +++++++++++++++++++++++++++++++++++ src/index.tsx | 2 + 3 files changed, 912 insertions(+) create mode 100644 src/NonSteamGamesSection.tsx diff --git a/main.py b/main.py index 6019754..d3a5432 100644 --- a/main.py +++ b/main.py @@ -1718,6 +1718,320 @@ async def list_installed_games(self) -> dict: decky.logger.error(str(e)) return {'status': 'error', 'message': str(e)} + def _parse_shortcuts_vdf(self, filepath: str) -> list: + """Parse Steam's binary shortcuts.vdf file and return a list of shortcut entries. + + Binary VDF format: + - 0x00 = nested object start (key is null-terminated string) + - 0x01 = string value (key and value are null-terminated) + - 0x02 = int32 value (key is null-terminated, value is 4 bytes LE) + - 0x08 = end of object + """ + entries = [] + try: + with open(filepath, 'rb') as f: + data = f.read() + + pos = 0 + size = len(data) + + def read_string(p): + end = data.index(b'\x00', p) + return data[p:end].decode('utf-8', errors='replace'), end + 1 + + def parse_object(p): + obj = {} + while p < size: + type_byte = data[p] + p += 1 + + if type_byte == 0x08: # End of object + break + + # Read key name + key, p = read_string(p) + + if type_byte == 0x00: # Nested object + value, p = parse_object(p) + obj[key] = value + elif type_byte == 0x01: # String value + value, p = read_string(p) + obj[key] = value + elif type_byte == 0x02: # Int32 value + value = int.from_bytes(data[p : p + 4], byteorder='little', signed=True) + p += 4 + obj[key] = value + else: + # Unknown type, skip + break + + return obj, p + + # Skip root object header: 0x00 + "shortcuts" + 0x00 + if data[pos] == 0x00: + pos += 1 + _, pos = read_string(pos) + + # Parse each shortcut entry + while pos < size: + type_byte = data[pos] + if type_byte == 0x08: # End of root object + break + if type_byte == 0x00: + pos += 1 + _, pos = read_string(pos) # Skip entry index key (e.g. "0", "1") + entry, pos = parse_object(pos) + entries.append(entry) + else: + break + + except Exception as e: + decky.logger.error(f'Error parsing shortcuts.vdf at {filepath}: {e!s}') + + return entries + + async def list_non_steam_games(self) -> dict: + """List non-Steam game shortcuts added via 'Add a Non-Steam Game' in Steam.""" + try: + steam_root = Path(decky.HOME) / '.steam' / 'steam' + userdata_path = steam_root / 'userdata' + + if not userdata_path.exists(): + return {'status': 'error', 'message': 'Steam userdata directory not found'} + + games = [] + seen_ids = set() + + # Iterate all user profiles + for user_dir in userdata_path.iterdir(): + if not user_dir.is_dir(): + continue + + shortcuts_file = user_dir / 'config' / 'shortcuts.vdf' + if not shortcuts_file.exists(): + continue + + entries = self._parse_shortcuts_vdf(str(shortcuts_file)) + for entry in entries: + app_name = entry.get('appname', entry.get('AppName', '')) + exe = entry.get('exe', entry.get('Exe', '')) + start_dir = entry.get('StartDir', '') + app_id = entry.get('appid', entry.get('AppId', 0)) + + if not app_name or not exe: + continue + + # Convert signed appid to unsigned for deduplication + unsigned_id = app_id & 0xFFFFFFFF if isinstance(app_id, int) else app_id + + if unsigned_id in seen_ids: + continue + seen_ids.add(unsigned_id) + + # Strip surrounding quotes from exe and start_dir + exe = exe.strip('"') + start_dir = start_dir.strip('"') + + games.append( + { + 'name': app_name, + 'exe': exe, + 'start_dir': start_dir, + 'appid': str(unsigned_id), + } + ) + + # Sort alphabetically + games.sort(key=lambda g: g['name'].lower()) + + return {'status': 'success', 'games': games} + + except Exception as e: + decky.logger.error(f'Error listing non-Steam games: {e!s}') + return {'status': 'error', 'message': str(e)} + + async def find_non_steam_game_executable_path( + self, exe_path: str, start_dir: str, game_name: str + ) -> dict: + """Find executable paths for a non-Steam game shortcut. + + If the shortcut's exe points to a .exe file, scan its directory for alternatives. + If it points to a script/flatpak command, try start_dir instead. + """ + try: + decky.logger.info( + f'Finding executable for non-Steam game: {game_name} (exe={exe_path}, start_dir={start_dir})' + ) + + # Check cache first + cache_key = f'nonsteam_{exe_path}_{game_name}' + if cache_key in self.executable_cache: + cached_result = self.executable_cache[cache_key] + if time.time() - cached_result.get('timestamp', 0) < 3600: + decky.logger.info(f'Using cached result for {game_name}') + return cached_result + + # Determine the search directory + search_dir = None + shortcut_is_exe = exe_path.lower().endswith('.exe') + + if shortcut_is_exe and os.path.exists(exe_path): + # The shortcut directly points to a .exe - use its directory + search_dir = os.path.dirname(exe_path) + elif start_dir and os.path.isdir(start_dir): + search_dir = start_dir + elif os.path.isdir(os.path.dirname(exe_path)): + search_dir = os.path.dirname(exe_path) + + if not search_dir or not os.path.isdir(search_dir): + # Can't find a valid directory to scan + if shortcut_is_exe and os.path.exists(exe_path): + # At least we have the exe itself + result = { + 'status': 'success', + 'non_steam_detection_result': { + 'status': 'success', + 'method': 'shortcut_exe', + 'executable_path': exe_path, + 'directory_path': os.path.dirname(exe_path), + 'filename': os.path.basename(exe_path), + 'all_executables': [ + { + 'path': exe_path, + 'directory_path': os.path.dirname(exe_path), + 'filename': os.path.basename(exe_path), + 'relative_path': os.path.basename(exe_path), + 'size': os.path.getsize(exe_path), + 'size_mb': round(os.path.getsize(exe_path) / (1024 * 1024), 1), + 'score': 100, + } + ], + 'confidence': 'high', + }, + 'is_script_shortcut': False, + 'recommended_method': 'shortcut_exe', + 'timestamp': time.time(), + } + self.executable_cache[cache_key] = result + return result + + return { + 'status': 'error', + 'method': 'non_steam_detection', + 'message': f'Could not find a valid game directory to scan. Exe: {exe_path}, StartDir: {start_dir}', + 'is_script_shortcut': not shortcut_is_exe, + } + + # Walk the directory tree and find all .exe files + all_executables = [] + decky.logger.info(f'Scanning directory: {search_dir}') + + for root, _dirs, files in os.walk(search_dir): + for file in files: + if file.lower().endswith('.exe'): + full_path = os.path.join(root, file) + try: + file_size = os.path.getsize(full_path) + rel_path = os.path.relpath(full_path, search_dir) + all_executables.append( + { + 'path': full_path, + 'directory_path': os.path.dirname(full_path), + 'relative_path': rel_path, + 'filename': file, + 'size': file_size, + 'size_mb': round(file_size / (1024 * 1024), 1), + } + ) + except Exception as e: + decky.logger.warning(f'Error getting size for {full_path}: {e!s}') + + if not all_executables: + return { + 'status': 'error', + 'method': 'non_steam_detection', + 'message': f'No .exe files found in {search_dir}', + 'is_script_shortcut': not shortcut_is_exe, + } + + decky.logger.info(f'Found {len(all_executables)} executables') + + # Score executables using the Heroic scorer (it's more generic than Steam's) + scored_executables = [] + for exe_info in all_executables: + score = score_heroic_executable(exe_info, game_name, search_dir, decky.logger) + + # Bonus if this exe matches the shortcut's exe path + if shortcut_is_exe and os.path.normpath(exe_info['path']) == os.path.normpath( + exe_path + ): + score += 50 + decky.logger.info( + f'Bonus +50 for matching shortcut exe: {exe_info["filename"]}' + ) + + if score > 0: + scored_executables.append({**exe_info, 'score': score}) + + if not scored_executables: + # Fallback: include all with any score + for exe_info in all_executables: + score = score_heroic_executable(exe_info, game_name, search_dir, decky.logger) + if shortcut_is_exe and os.path.normpath(exe_info['path']) == os.path.normpath( + exe_path + ): + score += 50 + scored_executables.append({**exe_info, 'score': score}) + + scored_executables.sort(key=lambda x: x['score'], reverse=True) + top_executables = scored_executables[:5] + best_executable = top_executables[0] + + decky.logger.info(f'Top executables for {game_name}:') + for i, exe in enumerate(top_executables): + decky.logger.info( + f' {i + 1}. {exe["filename"]} (score: {exe["score"]}) at {exe["relative_path"]}' + ) + + result = { + 'status': 'success', + 'non_steam_detection_result': { + 'status': 'success', + 'method': 'non_steam_detection', + 'executable_path': best_executable['path'], + 'directory_path': best_executable['directory_path'], + 'filename': best_executable['filename'], + 'all_executables': top_executables, + 'confidence': 'high' if best_executable['score'] > 70 else 'medium', + }, + 'is_script_shortcut': not shortcut_is_exe, + 'recommended_method': 'non_steam_detection', + 'timestamp': time.time(), + } + + self.executable_cache[cache_key] = result + return result + + except Exception as e: + decky.logger.error(f'Non-Steam executable detection error: {e!s}') + return {'status': 'error', 'method': 'non_steam_detection', 'message': str(e)} + + async def install_reshade_for_non_steam_game( + self, game_path: str, dll_override: str = 'dxgi', selected_executable_path: str = '' + ) -> dict: + """Install ReShade for a non-Steam game. Delegates to the Heroic installer since the logic is identical.""" + decky.logger.info( + f'Installing ReShade for non-Steam game at: {game_path} (exe: {selected_executable_path})' + ) + return await self.install_reshade_for_heroic_game( + game_path, dll_override, selected_executable_path + ) + + async def uninstall_reshade_for_non_steam_game(self, game_path: str) -> dict: + """Uninstall ReShade from a non-Steam game. Delegates to the Heroic uninstaller.""" + decky.logger.info(f'Uninstalling ReShade from non-Steam game at: {game_path}') + return await self.uninstall_reshade_for_heroic_game(game_path) + async def find_heroic_games(self) -> dict: """Find games installed through Heroic Launcher using the config file""" try: diff --git a/src/NonSteamGamesSection.tsx b/src/NonSteamGamesSection.tsx new file mode 100644 index 0000000..68dab39 --- /dev/null +++ b/src/NonSteamGamesSection.tsx @@ -0,0 +1,596 @@ +import { callable } from '@decky/api'; +import { + ButtonItem, + ConfirmModal, + DropdownItem, + PanelSection, + PanelSectionRow, + showModal, +} from '@decky/ui'; +import { useEffect, useState } from 'react'; + +// Define interfaces +interface NonSteamGameInfo { + name: string; + exe: string; + start_dir: string; + appid: string; +} + +interface DllOverride { + label: string; + value: string; +} + +interface NonSteamResponse { + status: string; + message?: string; + output?: string; + games?: NonSteamGameInfo[]; + api?: string; +} + +interface PathCheckResponse { + exists: boolean; + is_addon: boolean; +} + +interface ExecutableInfo { + path: string; + directory_path: string; + filename: string; + relative_path?: string; + score?: number; + size_mb?: number; +} + +interface DetectionResult { + status: string; + method?: string; + executable_path?: string; + directory_path?: string; + filename?: string; + all_executables?: ExecutableInfo[]; + confidence?: string; + message?: string; +} + +interface NonSteamExecutableDetectionResponse { + status: string; + non_steam_detection_result?: DetectionResult; + is_script_shortcut?: boolean; + recommended_method?: string; + message?: string; +} + +// Define callables +const listNonSteamGames = callable<[], NonSteamResponse>( + 'list_non_steam_games', +); +const findNonSteamGameExecutablePath = callable< + [string, string, string], + NonSteamExecutableDetectionResponse +>('find_non_steam_game_executable_path'); +const installReshadeForNonSteamGame = callable< + [string, string, string], + NonSteamResponse +>('install_reshade_for_non_steam_game'); +const uninstallReshadeForNonSteamGame = callable<[string], NonSteamResponse>( + 'uninstall_reshade_for_non_steam_game', +); +const detectHeroicGameApi = callable< + [string], + { status: string; api?: string; message?: string } +>('detect_heroic_game_api'); +const checkReShadePath = callable<[], PathCheckResponse>('check_reshade_path'); +const logError = callable<[string], void>('log_error'); + +const NonSteamGamesSection = () => { + const [games, setGames] = useState([]); + const [selectedGame, setSelectedGame] = useState( + null, + ); + const [selectedDll, setSelectedDll] = useState(null); + const [result, setResult] = useState(''); + const [loading, setLoading] = useState(true); + const [apiDetecting, setApiDetecting] = useState(false); + const [executableDetection, setExecutableDetection] = + useState(null); + const [checkingExecutable, setCheckingExecutable] = useState(false); + const [selectedExecutablePath, setSelectedExecutablePath] = + useState(''); + + const dllOverrides: DllOverride[] = [ + { label: 'Automatic (Detect API)', value: 'auto' }, + { label: 'DXGI (DirectX 10/11/12)', value: 'dxgi' }, + { label: 'D3D9 (DirectX 9)', value: 'd3d9' }, + { label: 'D3D8 (DirectX 8)', value: 'd3d8' }, + { label: 'D3D11 (DirectX 11)', value: 'd3d11' }, + { label: 'DDraw (DirectDraw)', value: 'ddraw' }, + { label: 'DInput8 (DirectInput)', value: 'dinput8' }, + { label: 'OpenGL32 (OpenGL)', value: 'opengl32' }, + ]; + + useEffect(() => { + const loadGames = async () => { + try { + setLoading(true); + const response = await listNonSteamGames(); + if (response.status === 'success' && response.games) { + setGames(response.games); + } else { + setResult( + `Failed to load non-Steam games: ${response.message || 'Unknown error'}`, + ); + } + } catch (error) { + setResult( + `Error loading non-Steam games: ${error instanceof Error ? error.message : String(error)}`, + ); + await logError(`NonSteamGamesSection -> loadGames: ${String(error)}`); + } finally { + setLoading(false); + } + }; + + loadGames(); + }, []); + + // Check executable detection when a game is selected + useEffect(() => { + const checkExecutableDetection = async () => { + if (!selectedGame) { + setExecutableDetection(null); + setSelectedExecutablePath(''); + return; + } + + try { + setCheckingExecutable(true); + const detection = await findNonSteamGameExecutablePath( + selectedGame.exe, + selectedGame.start_dir, + selectedGame.name, + ); + setExecutableDetection(detection); + + // Set default selected executable path based on detection + if ( + detection.status === 'success' && + detection.non_steam_detection_result?.status === 'success' + ) { + setSelectedExecutablePath( + detection.non_steam_detection_result.executable_path || '', + ); + } + } catch (error) { + await logError( + `Non-Steam executable detection error: ${String(error)}`, + ); + setExecutableDetection(null); + setSelectedExecutablePath(''); + } finally { + setCheckingExecutable(false); + } + }; + + checkExecutableDetection(); + }, [selectedGame]); + + const handleInstallReShade = async () => { + if (!selectedGame) { + setResult('Please select a game.'); + return; + } + + if (!selectedDll) { + setResult('Please select a DLL override or "Automatic".'); + return; + } + + try { + const reshadeCheck = await checkReShadePath(); + if (!reshadeCheck.exists) { + setResult('Please install ReShade first before patching games.'); + return; + } + + // Determine the game directory for installation + const gameDir = selectedExecutablePath + ? selectedExecutablePath.substring( + 0, + selectedExecutablePath.lastIndexOf('/'), + ) + : selectedGame.start_dir || + selectedGame.exe.substring(0, selectedGame.exe.lastIndexOf('/')); + + // If automatic is selected, detect the API + let finalDllOverride = selectedDll.value; + if (finalDllOverride === 'auto') { + setApiDetecting(true); + setResult('Detecting best API for your game...'); + + const detectionResponse = await detectHeroicGameApi(gameDir); + + if (detectionResponse.status === 'success' && detectionResponse.api) { + finalDllOverride = detectionResponse.api; + setResult( + `Detected ${finalDllOverride.toUpperCase()} as the best API for this game.`, + ); + } else { + finalDllOverride = 'dxgi'; + setResult( + `API detection failed: ${detectionResponse.message || 'Unknown error'}. Using DXGI as fallback.`, + ); + } + setApiDetecting(false); + } + + const getDetectionInfo = () => { + let info = `Are you sure you want to install ReShade for ${selectedGame.name} with ${finalDllOverride.toUpperCase()} API?`; + + if (selectedExecutablePath) { + const fileName = selectedExecutablePath.split('/').pop(); + info += `\n\nSelected executable: ${fileName}`; + info += `\nLocation: ${selectedExecutablePath}`; + } + + return info; + }; + + showModal( + { + setResult('Installing ReShade...'); + + const installResponse = await installReshadeForNonSteamGame( + gameDir, + finalDllOverride, + selectedExecutablePath, + ); + + if (installResponse.status === 'success') { + let successMessage = `ReShade installed successfully for ${selectedGame.name} with ${finalDllOverride.toUpperCase()} API.\nPress HOME key in-game to open ReShade overlay.`; + + if (selectedExecutablePath) { + const fileName = selectedExecutablePath.split('/').pop(); + successMessage += `\n\nInstalled to: ${fileName}`; + } + + setResult(successMessage); + } else { + setResult( + `Failed to install ReShade: ${installResponse.message || 'Unknown error'}`, + ); + } + }} + />, + ); + } catch (error) { + setResult( + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); + await logError( + `NonSteamGamesSection -> handleInstallReShade: ${String(error)}`, + ); + } + }; + + const handleUninstallReShade = async () => { + if (!selectedGame) { + setResult('Please select a game to uninstall ReShade from.'); + return; + } + + try { + const reshadeCheck = await checkReShadePath(); + if (!reshadeCheck.exists) { + setResult('ReShade is not installed.'); + return; + } + + const gameDir = selectedExecutablePath + ? selectedExecutablePath.substring( + 0, + selectedExecutablePath.lastIndexOf('/'), + ) + : selectedGame.start_dir || + selectedGame.exe.substring(0, selectedGame.exe.lastIndexOf('/')); + + showModal( + { + setResult('Uninstalling ReShade...'); + + const uninstallResponse = + await uninstallReshadeForNonSteamGame(gameDir); + + if (uninstallResponse.status === 'success') { + setResult( + `ReShade uninstalled successfully from ${selectedGame.name}.`, + ); + } else { + setResult( + `Failed to uninstall ReShade: ${uninstallResponse.message || 'Unknown error'}`, + ); + } + }} + />, + ); + } catch (error) { + setResult( + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); + await logError( + `NonSteamGamesSection -> handleUninstallReShade: ${String(error)}`, + ); + } + }; + + const renderExecutableSelection = () => { + if (!executableDetection || executableDetection.status !== 'success') + return null; + + const detectionResult = executableDetection.non_steam_detection_result; + + const executableOptions: Array<{ + path: string; + filename: string; + method: string; + isRecommended: boolean; + score?: number; + relative_path?: string; + displayLabel: string; + }> = []; + + if ( + detectionResult?.status === 'success' && + detectionResult.all_executables + ) { + detectionResult.all_executables.forEach((exe, index) => { + const isRecommended = exe.path === detectionResult.executable_path; + executableOptions.push({ + path: exe.path, + filename: exe.filename, + method: 'Non-Steam Detection', + isRecommended, + score: exe.score, + relative_path: exe.relative_path || `Directory ${index + 1}`, + displayLabel: `${exe.filename} ${isRecommended ? '(RECOMMENDED)' : ''} - ${exe.relative_path || 'Detected'} (Score: ${exe.score || 0})`, + }); + }); + } + + if (executableOptions.length === 0) return null; + + return ( + <> + +
+
+ Executable Detection Results ({executableOptions.length} found) +
+
+
+ + + ({ + data: option.path, + label: option.displayLabel, + }))} + selectedOption={selectedExecutablePath} + onChange={(option) => { + setSelectedExecutablePath(option.data); + }} + strDefaultLabel="Select executable location..." + /> + + + {selectedExecutablePath && + (() => { + const selectedOption = executableOptions.find( + (opt) => opt.path === selectedExecutablePath, + ); + if (!selectedOption) return null; + + return ( + +
+
+ Selected: {selectedOption.filename} + {selectedOption.isRecommended && ( + + RECOMMENDED + + )} +
+
+ Method: {selectedOption.method} + {selectedOption.score !== undefined && ( + + (Score: {selectedOption.score}) + + )} +
+
+ Path: {selectedOption.relative_path} +
+
+
+ ); + })()} + + {executableDetection.is_script_shortcut && ( + +
+ This shortcut points to a script or launcher, not a .exe file + directly. The detected executables above are from the start + directory. Please verify the correct executable is selected. +
+
+ )} + + ); + }; + + return ( + + {loading ? ( + +
Loading non-Steam games...
+
+ ) : games.length === 0 ? ( + +
+ No non-Steam game shortcuts found. Add games via Steam's "Add a + Non-Steam Game" option. +
+
+ ) : ( + <> + + ({ + data: game, + label: game.name, + }))} + selectedOption={selectedGame ? selectedGame : undefined} + onChange={(option) => { + setSelectedGame(option.data); + setResult(''); + }} + strDefaultLabel="Select a non-Steam game..." + /> + + + {selectedGame && checkingExecutable && ( + +
+ Analyzing game... Detecting executable +
+
+ )} + + {renderExecutableSelection()} + + {selectedGame && ( + + ({ + data: dll.value, + label: dll.label, + }))} + selectedOption={selectedDll ? selectedDll.value : undefined} + onChange={(option) => { + const selected = dllOverrides.find( + (dll) => dll.value === option.data, + ); + if (selected) { + setSelectedDll(selected); + setResult(''); + } + }} + strDefaultLabel="Select DLL override..." + /> + + )} + + {result && ( + +
+ {result} +
+
+ )} + + {selectedGame && ( + <> + + + {apiDetecting ? 'Detecting API...' : 'Install ReShade'} + + + + + Uninstall ReShade + + + + )} + + )} +
+ ); +}; + +export default NonSteamGamesSection; diff --git a/src/index.tsx b/src/index.tsx index 7fc9dff..4dc6736 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,6 +11,7 @@ import { import { useEffect, useState } from 'react'; import { IoMdColorPalette } from 'react-icons/io'; import HeroicGamesSection from './HeroicGamesSection'; +import NonSteamGamesSection from './NonSteamGamesSection'; import ShaderSelectionModal from './ShaderSelectionModal'; import SteamGamesSection from './SteamGamesSection'; import { getVersionOptions, VersionOption } from './versionOptions'; @@ -807,6 +808,7 @@ export default definePlugin(() => ({ <> + ), From 7021a3bfc34e189eda5914000a8dec3502c04c42 Mon Sep 17 00:00:00 2001 From: Mathias Eek <51080320+Cliffback@users.noreply.github.com> Date: Tue, 19 May 2026 20:28:22 +0200 Subject: [PATCH 2/3] refactor: filter non-Steam games to .exe shortcuts only Skip non-exe shortcuts (scripts, flatpaks, emulators, Heroic launcher entries) from the non-Steam games list since ReShade can only be injected into Windows executables. Remove is_script_shortcut warning logic that is no longer needed. --- main.py | 24 ++++++++---------------- src/NonSteamGamesSection.tsx | 19 ------------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/main.py b/main.py index d3a5432..2a0950f 100644 --- a/main.py +++ b/main.py @@ -1832,6 +1832,10 @@ async def list_non_steam_games(self) -> dict: exe = exe.strip('"') start_dir = start_dir.strip('"') + # Skip non-exe shortcuts (scripts, flatpaks, emulators, launchers) + if not exe.lower().endswith('.exe'): + continue + games.append( { 'name': app_name, @@ -1856,7 +1860,6 @@ async def find_non_steam_game_executable_path( """Find executable paths for a non-Steam game shortcut. If the shortcut's exe points to a .exe file, scan its directory for alternatives. - If it points to a script/flatpak command, try start_dir instead. """ try: decky.logger.info( @@ -1873,19 +1876,16 @@ async def find_non_steam_game_executable_path( # Determine the search directory search_dir = None - shortcut_is_exe = exe_path.lower().endswith('.exe') - if shortcut_is_exe and os.path.exists(exe_path): + if os.path.exists(exe_path): # The shortcut directly points to a .exe - use its directory search_dir = os.path.dirname(exe_path) elif start_dir and os.path.isdir(start_dir): search_dir = start_dir - elif os.path.isdir(os.path.dirname(exe_path)): - search_dir = os.path.dirname(exe_path) if not search_dir or not os.path.isdir(search_dir): # Can't find a valid directory to scan - if shortcut_is_exe and os.path.exists(exe_path): + if os.path.exists(exe_path): # At least we have the exe itself result = { 'status': 'success', @@ -1908,7 +1908,6 @@ async def find_non_steam_game_executable_path( ], 'confidence': 'high', }, - 'is_script_shortcut': False, 'recommended_method': 'shortcut_exe', 'timestamp': time.time(), } @@ -1919,7 +1918,6 @@ async def find_non_steam_game_executable_path( 'status': 'error', 'method': 'non_steam_detection', 'message': f'Could not find a valid game directory to scan. Exe: {exe_path}, StartDir: {start_dir}', - 'is_script_shortcut': not shortcut_is_exe, } # Walk the directory tree and find all .exe files @@ -1951,7 +1949,6 @@ async def find_non_steam_game_executable_path( 'status': 'error', 'method': 'non_steam_detection', 'message': f'No .exe files found in {search_dir}', - 'is_script_shortcut': not shortcut_is_exe, } decky.logger.info(f'Found {len(all_executables)} executables') @@ -1962,9 +1959,7 @@ async def find_non_steam_game_executable_path( score = score_heroic_executable(exe_info, game_name, search_dir, decky.logger) # Bonus if this exe matches the shortcut's exe path - if shortcut_is_exe and os.path.normpath(exe_info['path']) == os.path.normpath( - exe_path - ): + if os.path.normpath(exe_info['path']) == os.path.normpath(exe_path): score += 50 decky.logger.info( f'Bonus +50 for matching shortcut exe: {exe_info["filename"]}' @@ -1977,9 +1972,7 @@ async def find_non_steam_game_executable_path( # Fallback: include all with any score for exe_info in all_executables: score = score_heroic_executable(exe_info, game_name, search_dir, decky.logger) - if shortcut_is_exe and os.path.normpath(exe_info['path']) == os.path.normpath( - exe_path - ): + if os.path.normpath(exe_info['path']) == os.path.normpath(exe_path): score += 50 scored_executables.append({**exe_info, 'score': score}) @@ -2004,7 +1997,6 @@ async def find_non_steam_game_executable_path( 'all_executables': top_executables, 'confidence': 'high' if best_executable['score'] > 70 else 'medium', }, - 'is_script_shortcut': not shortcut_is_exe, 'recommended_method': 'non_steam_detection', 'timestamp': time.time(), } diff --git a/src/NonSteamGamesSection.tsx b/src/NonSteamGamesSection.tsx index 68dab39..650d07e 100644 --- a/src/NonSteamGamesSection.tsx +++ b/src/NonSteamGamesSection.tsx @@ -58,7 +58,6 @@ interface DetectionResult { interface NonSteamExecutableDetectionResponse { status: string; non_steam_detection_result?: DetectionResult; - is_script_shortcut?: boolean; recommended_method?: string; message?: string; } @@ -470,24 +469,6 @@ const NonSteamGamesSection = () => { ); })()} - - {executableDetection.is_script_shortcut && ( - -
- This shortcut points to a script or launcher, not a .exe file - directly. The detected executables above are from the start - directory. Please verify the correct executable is selected. -
-
- )} ); }; From 8f1d0a03a0b9dab6e0b38b4ca619f13d0bcb781e Mon Sep 17 00:00:00 2001 From: Mathias Eek <51080320+Cliffback@users.noreply.github.com> Date: Tue, 19 May 2026 20:52:18 +0200 Subject: [PATCH 3/3] style: rename section titles to remove redundant ReShade suffix --- src/HeroicGamesSection.tsx | 2 +- src/NonSteamGamesSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HeroicGamesSection.tsx b/src/HeroicGamesSection.tsx index 34a687a..ee7fc5f 100644 --- a/src/HeroicGamesSection.tsx +++ b/src/HeroicGamesSection.tsx @@ -563,7 +563,7 @@ const HeroicGamesSection = () => { }; return ( - + {loading ? (
Loading Heroic games...
diff --git a/src/NonSteamGamesSection.tsx b/src/NonSteamGamesSection.tsx index 650d07e..6f2d0d9 100644 --- a/src/NonSteamGamesSection.tsx +++ b/src/NonSteamGamesSection.tsx @@ -474,7 +474,7 @@ const NonSteamGamesSection = () => { }; return ( - + {loading ? (
Loading non-Steam games...