C#实现Steam游戏启动程序精准检索的技术问询
Hey there, great question—this is such a common pain point when scraping Steam game files. Recursive searches either miss valid nested exes (like ARK's) or flood you with junk from redist/bin folders. The fix? Use Steam's own metadata instead of guessing via file searches, since Steam tracks exactly which executable is the main game launcher for every title.
The Core Solution: Parse Steam's App Manifest Files
Every Steam game has an appmanifest_<AppID>.acf file stored in the steamapps directory of each library. This file holds official game metadata, including the path to its main executable. Here's how to leverage it:
Step 1: Extract Key Data from ACF Files
ACF files use a simple key-value format (similar to Valve's VDF files). For each game, we need two critical fields:
installdir: The name of the game's folder insteamapps/common/Executable: The relative path to the main game executable from theinstalldirfolder
For example, ARK: Survival Evolved's appmanifest will include:
"installdir" "ARK" "Launch" { "Executable" "ShooterGame\\Binaries\\Win64\\ShooterGame.exe" }
Step 2: Updated C# Code with Metadata Parsing
Here's a revised version of your SearchSteam method that integrates ACF parsing, plus fallback logic for edge cases where manifest data is missing:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Win32; public class SteamGameFinder { private List<string> steamGameDirs = new List<string>(); public void SearchSteam() { steamGameDirs.Clear(); // Get Steam install paths from registry string steam32 = "SOFTWARE\\VALVE\\"; string steam64 = "SOFTWARE\\Wow6432Node\\Valve\\"; // Process both 32-bit and 64-bit registry entries ProcessRegistryKey(Registry.LocalMachine.OpenSubKey(steam32)); ProcessRegistryKey(Registry.LocalMachine.OpenSubKey(steam64)); // Parse app manifests from each library's steamapps directory foreach (var libraryPath in steamGameDirs) { string steamAppsDir = Path.Combine(libraryPath, "steamapps"); if (!Directory.Exists(steamAppsDir)) continue; // Grab all appmanifest files string[] appManifests = Directory.GetFiles(steamAppsDir, "appmanifest_*.acf"); foreach (var manifestPath in appManifests) { ParseAppManifest(manifestPath, libraryPath); } } } private void ProcessRegistryKey(RegistryKey key) { if (key == null) return; foreach (string subKeyName in key.GetSubKeyNames()) { using (RegistryKey subKey = key.OpenSubKey(subKeyName)) { string installPath = subKey?.GetValue("InstallPath")?.ToString(); if (string.IsNullOrEmpty(installPath)) continue; // Add main Steam library, plus extract additional libraries from libraryfolders.vdf steamGameDirs.Add(installPath); ExtractAdditionalLibraries(Path.Combine(installPath, "steamapps", "libraryfolders.vdf")); } } } private void ExtractAdditionalLibraries(string libraryVdfPath) { if (!File.Exists(libraryVdfPath)) return; string[] configLines = File.ReadAllLines(libraryVdfPath); string driveRegex = @"[A-Z]:\\\\"; foreach (var line in configLines) { Match match = Regex.Match(line, driveRegex); if (match.Success) { string libraryPath = line.Substring(match.Index).Replace("\\\\", "\\").Trim('"', ' ', '\t'); if (!steamGameDirs.Contains(libraryPath)) { steamGameDirs.Add(libraryPath); } } } } private void ParseAppManifest(string manifestPath, string libraryPath) { string[] lines = File.ReadAllLines(manifestPath); string installDir = null; string executablePath = null; bool inLaunchSection = false; foreach (var line in lines) { string trimmedLine = line.Trim('\t', ' ', '"'); // Extract the game's install directory name if (trimmedLine.StartsWith("installdir")) { installDir = trimmedLine.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)[1]; } // Flag when we enter the Launch section else if (trimmedLine == "Launch") { inLaunchSection = true; } // Flag when we exit the Launch section else if (inLaunchSection && trimmedLine == "}") { inLaunchSection = false; } // Extract the main executable path from the Launch section else if (inLaunchSection && trimmedLine.StartsWith("Executable")) { executablePath = trimmedLine.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)[1]; break; // Stop searching once we find the main executable } } if (!string.IsNullOrEmpty(installDir) && !string.IsNullOrEmpty(executablePath)) { string gameInstallPath = Path.Combine(libraryPath, "steamapps", "common", installDir); string fullExecutablePath = Path.Combine(gameInstallPath, executablePath); if (File.Exists(fullExecutablePath)) { Console.WriteLine($"Game Title: {installDir}"); Console.WriteLine($"Main Executable: {fullExecutablePath}"); Console.WriteLine("---"); } else { // Fallback: If manifest path is invalid, run a filtered search Console.WriteLine($"Warning: Manifest path invalid for {installDir}, falling back to filtered search..."); FilteredExecutableSearch(gameInstallPath, installDir); } } else { // Fallback: No manifest data found, run filtered search string appId = Path.GetFileNameWithoutExtension(manifestPath).Replace("appmanifest_", ""); Console.WriteLine($"No manifest data found for AppID {appId}, falling back to filtered search..."); string possibleGameDir = Directory.EnumerateDirectories(Path.Combine(libraryPath, "steamapps", "common")) .FirstOrDefault(d => Path.GetFileName(d).Contains(appId)); if (possibleGameDir != null) { FilteredExecutableSearch(possibleGameDir, Path.GetFileName(possibleGameDir)); } } } private void FilteredExecutableSearch(string gameDir, string gameTitle) { // Filtered recursive search: avoid redist junk, prioritize exes matching the game title var validExes = Directory.GetFiles(gameDir, "*.exe", SearchOption.AllDirectories) .Where(exe => !exe.Contains("redist", StringComparison.OrdinalIgnoreCase) && !exe.Contains("_redist", StringComparison.OrdinalIgnoreCase) && // Allow bin/binaries folders only if the exe matches the game title (!exe.Contains("bin", StringComparison.OrdinalIgnoreCase) || Path.GetFileNameWithoutExtension(exe).Equals(gameTitle, StringComparison.OrdinalIgnoreCase)) && // Filter out tiny exes (adjust size threshold as needed) new FileInfo(exe).Length > 1024 * 1024 // 1MB minimum ); foreach (var exe in validExes) { Console.WriteLine($"Fallback Found: {exe}"); } Console.WriteLine("---"); } }
Why This Works Better
- Precision: Uses Steam's official metadata to get the exact main executable, so you never miss games like ARK or pull in redist clutter.
- Fallback Logic: For rare games with missing manifest data, the filtered search balances completeness and cleanliness.
- Scalability: Works across all Steam libraries, not just the default installation folder.
Additional Tips
- Some games have multiple launch options (e.g., editors, dedicated servers). If you need these, modify the
ParseAppManifestmethod to collect allExecutableentries in theLaunchsection instead of breaking after the first one. - For more robust ACF parsing (handling deeply nested sections), consider using a dedicated VDF/ACF parser library, but the simple line parsing above works for most cases.
内容的提问来源于stack exchange,提问作者Pete Kemp

