diff --git a/CreamInstaller/Forms/SelectForm.cs b/CreamInstaller/Forms/SelectForm.cs index 0e092ad..1c897c7 100644 --- a/CreamInstaller/Forms/SelectForm.cs +++ b/CreamInstaller/Forms/SelectForm.cs @@ -569,6 +569,8 @@ internal sealed partial class SelectForm : CustomForm progressLabel.Text = "Waiting for user to select which programs/games to scan . . ."; ShowProgressBar(); await ProgramData.Setup(this); + ProgramData.ClearLog(); + ProgramData.Log($"[Scan] CreamInstaller {Program.Version} — scan started at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); bool scan = forceScan; if (!scan && (programsToScan is null || programsToScan.Count < 1 || forceProvideChoices)) { diff --git a/CreamInstaller/Platforms/Steam/SteamLibrary.cs b/CreamInstaller/Platforms/Steam/SteamLibrary.cs index c87e0af..7d20395 100644 --- a/CreamInstaller/Platforms/Steam/SteamLibrary.cs +++ b/CreamInstaller/Platforms/Steam/SteamLibrary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using CreamInstaller.Utility; @@ -28,16 +29,19 @@ internal static class SteamLibrary { List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new(); HashSet gameLibraryDirectories = await GetLibraryDirectories(); + ProgramData.Log($"[Steam] Found {gameLibraryDirectories.Count} library folder(s)."); foreach (string libraryDirectory in gameLibraryDirectories) { if (Program.Canceled) return games; + ProgramData.Log($"[Steam] Scanning library: {libraryDirectory}"); foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in (await GetGamesFromLibraryDirectory( libraryDirectory)).Where(game => games.All(_game => _game.appId != game.appId))) games.Add(game); } + ProgramData.Log($"[Steam] Total games detected: {games.Count}"); return games; }); @@ -47,13 +51,21 @@ internal static class SteamLibrary { List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new(); if (Program.Canceled || !libraryDirectory.DirectoryExists()) + { + ProgramData.Log($"[Steam] Skipping library (not found or canceled): {libraryDirectory}"); return games; + } + foreach (string file in libraryDirectory.EnumerateDirectory("*.acf")) { if (Program.Canceled) return games; if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result)) + { + ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}"); continue; + } + string appId = result.Value.GetChild("appid")?.ToString(); string installdir = result.Value.GetChild("installdir")?.ToString(); string name = result.Value.GetChild("name")?.ToString(); @@ -61,11 +73,23 @@ internal static class SteamLibrary if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(buildId)) + { + ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}"); continue; - string gameDirectory = (libraryDirectory + @"\common\" + installdir).ResolvePath(); - if (gameDirectory is null || !int.TryParse(appId, out int _) || - !int.TryParse(buildId, out int buildIdInt) || games.Any(g => g.appId == appId)) + } + + string rawGameDirectory = libraryDirectory + @"\common\" + installdir; + string gameDirectory = rawGameDirectory.ResolvePath(); + if (gameDirectory is null) + { + ProgramData.Log($"[Steam] Game directory not found (drive may be slow or disconnected): {rawGameDirectory} | App: {name} ({appId})"); continue; + } + + if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt) || + games.Any(g => g.appId == appId)) + continue; + VToken userConfig = result.Value.GetChild("UserConfig"); string branch = userConfig?.GetChild("BetaKey")?.ToString(); branch ??= userConfig?.GetChild("betakey")?.ToString(); @@ -78,6 +102,8 @@ internal static class SteamLibrary if (string.IsNullOrWhiteSpace(branch)) branch = "public"; + + ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {gameDirectory}"); games.Add((appId, name, branch, buildIdInt, gameDirectory)); } @@ -92,25 +118,50 @@ internal static class SteamLibrary return libraryDirectories; string steamInstallPath = InstallPath; if (steamInstallPath == null || !steamInstallPath.DirectoryExists()) + { + ProgramData.Log($"[Steam] Steam install path not found or inaccessible: {steamInstallPath ?? "(null)"}"); return libraryDirectories; + } + string libraryFolder = steamInstallPath + @"\steamapps"; if (!libraryFolder.DirectoryExists()) + { + ProgramData.Log($"[Steam] Default steamapps folder not found: {libraryFolder}"); return libraryDirectories; + } + _ = libraryDirectories.Add(libraryFolder); + ProgramData.Log($"[Steam] Default library folder: {libraryFolder}"); + string libraryFolders = libraryFolder + @"\libraryfolders.vdf"; if (!libraryFolders.FileExists() || !ValveDataFile.TryDeserialize(libraryFolders.ReadFile(), out VProperty result)) + { + ProgramData.Log($"[Steam] libraryfolders.vdf not found or failed to parse: {libraryFolders}"); return libraryDirectories; + } + foreach (VToken vToken in result.Value.Where(p => p is VProperty property && int.TryParse(property.Key, out int _))) { VProperty property = (VProperty)vToken; - string path = property.Value.GetChild("path")?.ToString(); - if (string.IsNullOrWhiteSpace(path)) + string rawPath = property.Value.GetChild("path")?.ToString(); + if (string.IsNullOrWhiteSpace(rawPath)) continue; - path += @"\steamapps"; - if (path.DirectoryExists()) - _ = libraryDirectories.Add(path); + + // Normalize the path from VDF (may use forward slashes or wrong casing) + string normalizedPath = Path.GetFullPath(rawPath); + string steamappsPath = normalizedPath + @"\steamapps"; + string resolvedPath = steamappsPath.ResolvePath(); + + if (resolvedPath is null) + { + ProgramData.Log($"[Steam] External library not accessible (drive may be disconnected or letter changed): {steamappsPath}"); + continue; + } + + if (libraryDirectories.Add(resolvedPath)) + ProgramData.Log($"[Steam] Additional library folder found: {resolvedPath}"); } return libraryDirectories; diff --git a/CreamInstaller/Utility/Diagnostics.cs b/CreamInstaller/Utility/Diagnostics.cs index c9cdb25..800bb24 100644 --- a/CreamInstaller/Utility/Diagnostics.cs +++ b/CreamInstaller/Utility/Diagnostics.cs @@ -54,7 +54,16 @@ internal static class Diagnostics if (info.Parent is null) return info.Name.ToUpperInvariant(); string parent = ResolvePath(info.Parent.FullName); - string name = info.Parent.GetFileSystemInfos(info.Name)[0].Name; - return parent is null ? name : Path.Combine(parent, name); + try + { + FileSystemInfo[] infos = info.Parent.GetFileSystemInfos(info.Name); + string name = infos.Length > 0 ? infos[0].Name : info.Name; + return parent is null ? name : Path.Combine(parent, name); + } + catch + { + // Fall back to the raw name if the filesystem call fails (e.g. on a slow external drive) + return parent is null ? info.Name : Path.Combine(parent, info.Name); + } } } \ No newline at end of file diff --git a/CreamInstaller/Utility/ProgramData.cs b/CreamInstaller/Utility/ProgramData.cs index 1e80c9a..ef6c55b 100644 --- a/CreamInstaller/Utility/ProgramData.cs +++ b/CreamInstaller/Utility/ProgramData.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json; @@ -28,6 +30,38 @@ internal static class ProgramData private static readonly string DlcChoicesPath = DirectoryPath + @"\dlc.json"; private static readonly string KoaloaderProxyChoicesPath = DirectoryPath + @"\proxies.json"; + internal static readonly string LogPath = DirectoryPath + @"\scan.log"; + + private static readonly object LogLock = new(); + + internal static void Log(string message) + { + try + { + string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); + string entry = $"[{timestamp}] {message}{Environment.NewLine}"; + lock (LogLock) + File.AppendAllText(LogPath, entry, Encoding.UTF8); + } + catch + { + // ignored — logging must never crash the application + } + } + + internal static void ClearLog() + { + try + { + if (File.Exists(LogPath)) + File.Delete(LogPath); + } + catch + { + // ignored + } + } + internal static async Task Setup(Form form = null) => await Task.Run(() => {