From f007542b309c8c963bce89598406f38a930f33af Mon Sep 17 00:00:00 2001 From: Ioannis Date: Tue, 24 Mar 2026 16:47:12 +0000 Subject: [PATCH 1/4] Enhance game launch process with error handling Added error handling and logging for game launch process, including checks for update operations and game executable existence. --- Wauncher/ViewModels/MainWindowViewModel.cs | 46 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/Wauncher/ViewModels/MainWindowViewModel.cs b/Wauncher/ViewModels/MainWindowViewModel.cs index dc67596..8a6bd7a 100644 --- a/Wauncher/ViewModels/MainWindowViewModel.cs +++ b/Wauncher/ViewModels/MainWindowViewModel.cs @@ -99,7 +99,12 @@ public partial class MainWindowViewModel : ViewModelBase private async Task LaunchGameAsync() { if (_updateService.IsInstalling || _updateService.IsUpdating || _updateService.IsCheckingUpdates) + { + string errorMsg = "Launch blocked: Update operation in progress"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("MainWindowViewModel.LaunchGameAsync", errorMsg, "User attempted to launch during update"); return; + } if (_updateService.IsNeedingInstall) { @@ -115,8 +120,19 @@ private async Task LaunchGameAsync() if (_gameService.IsRunning()) { - ConsoleManager.ShowError( - "ClassicCounter is already running.\n\nPlease close the game before joining a server from Wauncher."); + string errorMsg = "ClassicCounter is already running.\n\nPlease close the game before joining a server from Wauncher."; + ConsoleManager.ShowError(errorMsg); + ErrorLogger.LogError("MainWindowViewModel.LaunchGameAsync", "Game already running", "User attempted to launch while game is running"); + return; + } + + // Validate game executable exists before attempting launch + string gameExePath = Path.Combine(Directory.GetCurrentDirectory(), "csgo.exe"); + if (!File.Exists(gameExePath)) + { + string errorMsg = "Game executable not found!\n\ncsgo.exe is missing from the launcher directory.\nPlease verify your game files or reinstall."; + ConsoleManager.ShowError(errorMsg); + ErrorLogger.LogError("MainWindowViewModel.LaunchGameAsync", "Game executable not found", $"Path: {gameExePath}"); return; } @@ -130,11 +146,25 @@ private async Task LaunchGameAsync() // Clear any arguments left over from a previous launch before adding new ones. _gameService.ClearAdditionalArguments(); - var connectTarget = selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort) - ? selected.IpPort - : null; + string? connectTarget = null; + if (selected != null && !selected.IsNone && !string.IsNullOrEmpty(selected.IpPort)) + { + connectTarget = selected.IpPort; + Terminal.Print($"Connecting to server: {selected.Name} ({selected.IpPort})"); + } + else + { + Terminal.Print("Launching game without server connection"); + } - await _gameService.LaunchAsync(connectTarget, settings.LaunchOptions); + bool launchSuccess = await _gameService.LaunchAsync(connectTarget, settings.LaunchOptions); + + if (!launchSuccess) + { + ConsoleManager.ShowError("Failed to launch game. Please check the logs for details."); + ErrorLogger.LogError("MainWindowViewModel.LaunchGameAsync", "Game launch returned false", $"Connect target: {connectTarget}, Launch options: {settings.LaunchOptions}"); + return; + } if (settings.DiscordRpc) { @@ -147,7 +177,9 @@ await _discordService.SetDetailsAsync((selected != null && !selected.IsNone) } catch (Exception ex) { - ConsoleManager.ShowError($"Failed to launch game:\n{ex.Message}"); + string errorMsg = $"Failed to launch game:\n{ex.Message}"; + ConsoleManager.ShowError(errorMsg); + ErrorLogger.LogError("MainWindowViewModel.LaunchGameAsync", ex, $"Connect target: {SelectedServer?.IpPort}, Launch options: {SettingsWindowViewModel.LoadGlobal().LaunchOptions}"); } finally { From 685375de0fc881050cc343910cbd3f50314370fd Mon Sep 17 00:00:00 2001 From: Ioannis Date: Tue, 24 Mar 2026 16:48:01 +0000 Subject: [PATCH 2/4] Enhance update check with error handling and logging --- Wauncher/Services/UpdateService.cs | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Wauncher/Services/UpdateService.cs b/Wauncher/Services/UpdateService.cs index 99e053d..7afd1a9 100644 --- a/Wauncher/Services/UpdateService.cs +++ b/Wauncher/Services/UpdateService.cs @@ -50,7 +50,12 @@ public partial class UpdateService : ObservableObject, IUpdateService public async Task CheckForUpdatesAsync() { if (IsCheckingUpdates || IsUpdating || IsInstalling) + { + string errorMsg = "Update check already in progress"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("UpdateService.CheckForUpdatesAsync", errorMsg, "Multiple update check attempts"); return false; + } IsCheckingUpdates = true; @@ -60,21 +65,41 @@ public async Task CheckForUpdatesAsync() if (!File.Exists(csgoExe)) { + string errorMsg = "Game executable not found - installation needed"; + Terminal.Print(errorMsg); + ErrorLogger.LogError("UpdateService.CheckForUpdatesAsync", errorMsg, $"Expected path: {csgoExe}"); IsNeedingInstall = true; return true; } + Terminal.Print("Checking for updates..."); var patches = await GetPatchesAsync(); if (patches == null) + { + string errorMsg = "Failed to retrieve patch information"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("UpdateService.CheckForUpdatesAsync", errorMsg, "GetPatchesAsync returned null"); return false; + } var needsUpdate = await ValidateFilesAsync(patches); IsUpdateAvailable = needsUpdate; + if (needsUpdate) + { + Terminal.Print("Updates are available"); + } + else + { + Terminal.Print("Game is up to date"); + } + return needsUpdate; } catch (Exception ex) { + string errorMsg = $"Update check failed: {ex.Message}"; + Terminal.Error(errorMsg); ErrorLogger.LogError("UpdateService.CheckForUpdatesAsync", ex, "Failed to check for updates"); return false; } @@ -248,16 +273,34 @@ await DownloadManager.DownloadPatch( private async Task GetPatchesAsync() { if (_cachedPatches != null) + { + Terminal.Print("Using cached patch information"); return _cachedPatches; + } try { + Terminal.Print("Fetching patch information from API..."); var patches = await PatchManager.ValidatePatches(); - _cachedPatches = patches; + + if (patches.Success) + { + _cachedPatches = patches; + Terminal.Print($"Patch validation complete: {patches.Missing.Count} missing, {patches.Outdated.Count} outdated"); + } + else + { + string errorMsg = "Patch validation failed"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("UpdateService.GetPatchesAsync", errorMsg, "PatchManager.ValidatePatches returned Success=false"); + } + return patches; } catch (Exception ex) { + string errorMsg = $"Failed to get patches: {ex.Message}"; + Terminal.Error(errorMsg); ErrorLogger.LogError("UpdateService.GetPatchesAsync", ex, "Failed to get patches"); return null; } From 9f1007ad65c059ec6042153d4e8d48d03ac61c82 Mon Sep 17 00:00:00 2001 From: Ioannis Date: Tue, 24 Mar 2026 16:48:38 +0000 Subject: [PATCH 3/4] Refactor game launch process and error handling --- Wauncher/Utils/Game.cs | 64 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/Wauncher/Utils/Game.cs b/Wauncher/Utils/Game.cs index 7281742..4e0ff96 100644 --- a/Wauncher/Utils/Game.cs +++ b/Wauncher/Utils/Game.cs @@ -44,7 +44,8 @@ public static void QueueDeferredConnect(string ipPort) public static async Task Launch() { List arguments = Argument.GenerateGameArguments(); - if (arguments.Count > 0) Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); + if (arguments.Count > 0) + Terminal.Print($"Arguments: {string.Join(" ", arguments)}"); var settings = ViewModels.SettingsWindowViewModel.LoadGlobal(); string directory = Path.GetDirectoryName(Services.GetExePath()) ?? Directory.GetCurrentDirectory(); @@ -58,6 +59,14 @@ public static async Task Launch() try { + // Ensure the cfg directory exists + string cfgDirectory = Path.GetDirectoryName(gameStatePath) ?? ""; + if (!Directory.Exists(cfgDirectory)) + { + Directory.CreateDirectory(cfgDirectory); + Terminal.Print($"Created config directory: {cfgDirectory}"); + } + string gameStateContents = $$""" "ClassicCounter" { @@ -83,22 +92,36 @@ public static async Task Launch() } """; await File.WriteAllTextAsync(gameStatePath, gameStateContents); + Terminal.Print("Game state integration config written successfully"); } catch (Exception ex) { + string errorMsg = "Failed to create game state integration config"; ErrorLogger.LogError("Game.Launch", ex, "Failed to write gamestate integration config"); - Terminal.Error("(!) \"/csgo/cfg/gamestate_integration_cc.cfg\" not found in the current directory!"); + Terminal.Error($"(!) \"/csgo/cfg/gamestate_integration_cc.cfg\" could not be created!"); + ConsoleManager.ShowError($"{errorMsg}:\n{ex.Message}\n\nDiscord RPC and auto-connect features may not work properly."); } } else if (File.Exists(gameStatePath)) { - File.Delete(gameStatePath); + try + { + File.Delete(gameStatePath); + Terminal.Print("Removed game state integration config (disabled)"); + } + catch (Exception ex) + { + string errorMsg = "Failed to delete game state integration config"; + ErrorLogger.LogError("Game.Launch", ex, "Failed to delete gamestate integration config"); + Terminal.Warning("Could not delete game state integration config file"); + } } _process = new Process(); string gameExe = "csgo.exe"; - _process.StartInfo.FileName = Path.Combine(directory, gameExe); + string gameExePath = Path.Combine(directory, gameExe); + _process.StartInfo.FileName = gameExePath; _process.StartInfo.Arguments = string.Join(" ", arguments); _process.StartInfo.WorkingDirectory = directory; @@ -111,12 +134,39 @@ public static async Task Launch() if (!File.Exists(_process.StartInfo.FileName)) { - Terminal.Error($"(!) {gameExe} not found in the current directory!"); - ConsoleManager.ShowError($"{gameExe} not found in the current directory!\n\nPlease make sure the launcher and game files are in the same folder."); + string errorMsg = $"{gameExe} not found in current directory!\n\nExpected path: {_process.StartInfo.FileName}"; + Terminal.Error($"(!) {errorMsg}"); + ConsoleManager.ShowError(errorMsg); + ErrorLogger.LogError("Game.Launch", "Game executable not found", $"Path: {_process.StartInfo.FileName}"); return false; } - return _process.Start(); + try + { + Terminal.Print($"Starting process: {_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); + bool started = _process.Start(); + + if (started) + { + Terminal.Print("Game process started successfully"); + } + else + { + string errorMsg = "Failed to start game process (Process.Start returned false)"; + Terminal.Error(errorMsg); + ErrorLogger.LogError("Game.Launch", errorMsg, $"Executable: {_process.StartInfo.FileName}, Arguments: {_process.StartInfo.Arguments}"); + } + + return started; + } + catch (Exception ex) + { + string errorMsg = $"Failed to start game process:\n{ex.Message}"; + Terminal.Error($"(!) {errorMsg}"); + ConsoleManager.ShowError(errorMsg); + ErrorLogger.LogError("Game.Launch", ex, $"Executable: {_process.StartInfo.FileName}, Arguments: {_process.StartInfo.Arguments}"); + return false; + } } public static async Task Monitor() From bc3fa2102c907fd1f19d0893f94382bc887ab780 Mon Sep 17 00:00:00 2001 From: Ioannis Date: Tue, 24 Mar 2026 16:49:07 +0000 Subject: [PATCH 4/4] Improve error handling in PatchManager.GetPatches Enhanced error handling for API response in GetPatches method. --- Wauncher/Utils/Patch.cs | 522 ++++++++++++++++++++++------------------ 1 file changed, 283 insertions(+), 239 deletions(-) diff --git a/Wauncher/Utils/Patch.cs b/Wauncher/Utils/Patch.cs index 76d8175..cd9792e 100644 --- a/Wauncher/Utils/Patch.cs +++ b/Wauncher/Utils/Patch.cs @@ -1,240 +1,284 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Collections.Concurrent; -using System.Security.Cryptography; - -namespace Wauncher.Utils -{ - public class Patch - { - [JsonProperty(PropertyName = "file")] - public required string File { get; set; } - - [JsonProperty(PropertyName = "hash")] - public required string Hash { get; set; } - }; - - public class Patches(bool success, List missing, List outdated) - { - public bool Success = success; - public List Missing = missing; - public List Outdated = outdated; - } - - public static class PatchManager - { - private static string GetOriginalFileName(string fileName) - { - return fileName.EndsWith(".7z") ? fileName[..^3] : fileName; - } - - private static async Task> GetPatches(bool validateAll = false) - { - List patches = new List(); - - try - { - string responseString = await Api.ClassicCounter.GetPatches(); - - JObject responseJson = JObject.Parse(responseString); - - if (responseJson["files"] != null) - patches = responseJson["files"]!.ToObject()!.ToList(); - } - catch - { - if (Debug.Enabled()) - Terminal.Debug($"Couldn't get {(validateAll ? "full game" : "patch")} API data."); - } - - return patches; - } - - private static async Task GetHash(string filePath) - { - using var md5 = MD5.Create(); - await using var stream = File.OpenRead(filePath); - byte[] hash = await Task.Run(() => md5.ComputeHash(stream)); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - - public static async Task ValidatePatches(bool validateAll = false, bool deleteOutdatedFiles = true) - { - List patches = await GetPatches(validateAll); - List missing = new(); - List outdated = new(); - Patch? dirPatch = null; - - // first only check pak_dat.vpk - var pakDatPatch = patches.FirstOrDefault(p => p.File == "csgo/pak_dat.vpk"); - bool skipValidation = false; - - if (pakDatPatch != null && !validateAll) - { - string pakDatPath = $"{Directory.GetCurrentDirectory()}/csgo/pak_dat.vpk"; - - if (Debug.Enabled()) - Terminal.Debug("Checking csgo/pak_dat.vpk first..."); - - if (File.Exists(pakDatPath)) - { - if (Debug.Enabled()) - Terminal.Debug("Checking hash for: csgo/pak_dat.vpk"); - - string pakDatHash = await GetHash(pakDatPath); - if (pakDatHash == pakDatPatch.Hash) - { - if (Debug.Enabled()) - Terminal.Debug("csgo/pak_dat.vpk is up to date - skipping other file checks"); - skipValidation = true; - return new Patches(true, missing, outdated); - } - else - { - if (Debug.Enabled()) - Terminal.Debug("csgo/pak_dat.vpk is outdated - will check all files"); - if (deleteOutdatedFiles) - File.Delete(pakDatPath); - } - } - else - { - if (Debug.Enabled()) - Terminal.Debug("Missing: csgo/pak_dat.vpk - will check all files"); - } - } - - if (!skipValidation) - { - // find pak01_dir.vpk from patch api - dirPatch = patches.FirstOrDefault(p => p.File.Contains("pak01_dir.vpk")); - bool needPak01Update = false; - - if (dirPatch != null) - { - string dirPath = $"{Directory.GetCurrentDirectory()}/csgo/pak01_dir.vpk"; - - if (Debug.Enabled()) - Terminal.Debug("Checking csgo/pak01_dir.vpk first..."); - - if (File.Exists(dirPath)) - { - if (Debug.Enabled()) - Terminal.Debug("Checking hash for: csgo/pak01_dir.vpk"); - - string dirHash = await GetHash(dirPath); - if (dirHash != dirPatch.Hash) - { - if (Debug.Enabled()) - Terminal.Debug("csgo/pak01_dir.vpk is outdated!"); - - if (deleteOutdatedFiles) - File.Delete(dirPath); - outdated.Add(dirPatch); - needPak01Update = true; - } - else if (!validateAll) - { - if (Debug.Enabled()) - Terminal.Debug("csgo/pak01_dir.vpk is up to date - will skip pak01 files"); - } - else - { - if (Debug.Enabled()) - Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to full validation mode"); - } - } - else - { - if (Debug.Enabled()) - Terminal.Debug("Missing: csgo/pak01_dir.vpk!"); - - missing.Add(dirPatch); - needPak01Update = true; - } - - if (!needPak01Update) - { - patches.Remove(dirPatch); - } - } - - var concurrentMissing = new ConcurrentBag(); - var concurrentOutdated = new ConcurrentBag(); - - var parallelOptions = new ParallelOptions - { - MaxDegreeOfParallelism = 4 - }; - - await Parallel.ForEachAsync(patches, parallelOptions, async (patch, cancellationToken) => - { - string originalFileName = GetOriginalFileName(patch.File); - - // skip dir file (we already checked it) - if (originalFileName.Contains("pak01_dir.vpk")) - return; - - // are you a pak01 file? - bool isPak01File = originalFileName.Contains("pak01_"); - string path = Path.Combine(Directory.GetCurrentDirectory(), originalFileName); - - if (isPak01File && !needPak01Update && !validateAll) - { - if (!File.Exists(path)) - { - if (Debug.Enabled()) - Terminal.Debug($"Missing: {originalFileName}"); - - concurrentMissing.Add(patch); - return; - } - - if (Debug.Enabled()) - Terminal.Debug($"Skipping hash check for: {originalFileName} (pak01_dir.vpk up to date)"); - - return; - } - - if (!File.Exists(path)) - { - if (Debug.Enabled()) - Terminal.Debug($"Missing: {originalFileName}"); - - concurrentMissing.Add(patch); - return; - } - - if (Debug.Enabled()) - Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && validateAll ? " (full validation)" : "")}"); - - string hash = await GetHash(path); - if (hash != patch.Hash) - { - if (Debug.Enabled()) - Terminal.Debug($"Outdated: {originalFileName}"); - - if (deleteOutdatedFiles) - File.Delete(path); - concurrentOutdated.Add(patch); - } - }); - - missing.AddRange(concurrentMissing); - outdated.AddRange(concurrentOutdated); - - // if pak01_dir.vpk needs update, move it to end of lists - if (needPak01Update && dirPatch != null) - { - if (outdated.Remove(dirPatch)) - outdated.Add(dirPatch); - if (missing.Remove(dirPatch)) - missing.Add(dirPatch); - } - } - - return new Patches(patches.Count > 0, missing, outdated); - } - } -} +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Concurrent; +using System.Security.Cryptography; + +namespace Wauncher.Utils +{ + public class Patch + { + [JsonProperty(PropertyName = "file")] + public required string File { get; set; } + + [JsonProperty(PropertyName = "hash")] + public required string Hash { get; set; } + }; + + public class Patches(bool success, List missing, List outdated) + { + public bool Success = success; + public List Missing = missing; + public List Outdated = outdated; + } + + public static class PatchManager + { + private static string GetOriginalFileName(string fileName) + { + return fileName.EndsWith(".7z") ? fileName[..^3] : fileName; + } + + private static async Task> GetPatches(bool validateAll = false) + { + List patches = new List(); + + try + { + string responseString = await Api.ClassicCounter.GetPatches(); + + if (string.IsNullOrWhiteSpace(responseString)) + { + string errorMsg = "Received empty response from patches API"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", errorMsg, validateAll ? "Full game validation" : "Patch validation"); + return patches; + } + + JObject responseJson = JObject.Parse(responseString); + + if (responseJson["files"] == null) + { + string errorMsg = "API response missing 'files' field"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", errorMsg, $"Response: {responseString}"); + return patches; + } + + var filesArray = responseJson["files"]!.ToObject(); + if (filesArray == null || filesArray.Length == 0) + { + string errorMsg = "No files found in API response"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", errorMsg, $"Response contained {filesArray?.Length ?? 0} files"); + return patches; + } + + patches = filesArray.ToList(); + Terminal.Print($"Loaded {patches.Count} patches from API"); + } + catch (Newtonsoft.Json.JsonException ex) + { + string errorMsg = "Failed to parse patches API response"; + Terminal.Error(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", ex, "JSON parsing error"); + } + catch (System.Net.Http.HttpRequestException ex) + { + string errorMsg = "Network error while fetching patches"; + Terminal.Error(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", ex, "Network request failed"); + } + catch (System.Threading.Tasks.TaskCanceledException ex) + { + string errorMsg = "Request timeout while fetching patches"; + Terminal.Error(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", ex, "Request timeout"); + } + catch (Exception ex) + { + string errorMsg = $"Unexpected error getting {(validateAll ? "full game" : "patch")} API data: {ex.Message}"; + Terminal.Warning(errorMsg); + ErrorLogger.LogError("PatchManager.GetPatches", ex, validateAll ? "Full game validation" : "Patch validation"); + } + + return patches; + } + + private static async Task GetHash(string filePath) + { + using var md5 = MD5.Create(); + await using var stream = File.OpenRead(filePath); + byte[] hash = await Task.Run(() => md5.ComputeHash(stream)); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + public static async Task ValidatePatches(bool validateAll = false, bool deleteOutdatedFiles = true) + { + List patches = await GetPatches(validateAll); + List missing = new(); + List outdated = new(); + Patch? dirPatch = null; + + // first only check pak_dat.vpk + var pakDatPatch = patches.FirstOrDefault(p => p.File == "csgo/pak_dat.vpk"); + bool skipValidation = false; + + if (pakDatPatch != null && !validateAll) + { + string pakDatPath = $"{Directory.GetCurrentDirectory()}/csgo/pak_dat.vpk"; + + if (Debug.Enabled()) + Terminal.Debug("Checking csgo/pak_dat.vpk first..."); + + if (File.Exists(pakDatPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Checking hash for: csgo/pak_dat.vpk"); + + string pakDatHash = await GetHash(pakDatPath); + if (pakDatHash == pakDatPatch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak_dat.vpk is up to date - skipping other file checks"); + skipValidation = true; + return new Patches(true, missing, outdated); + } + else + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak_dat.vpk is outdated - will check all files"); + if (deleteOutdatedFiles) + File.Delete(pakDatPath); + } + } + else + { + if (Debug.Enabled()) + Terminal.Debug("Missing: csgo/pak_dat.vpk - will check all files"); + } + } + + if (!skipValidation) + { + // find pak01_dir.vpk from patch api + dirPatch = patches.FirstOrDefault(p => p.File.Contains("pak01_dir.vpk")); + bool needPak01Update = false; + + if (dirPatch != null) + { + string dirPath = $"{Directory.GetCurrentDirectory()}/csgo/pak01_dir.vpk"; + + if (Debug.Enabled()) + Terminal.Debug("Checking csgo/pak01_dir.vpk first..."); + + if (File.Exists(dirPath)) + { + if (Debug.Enabled()) + Terminal.Debug("Checking hash for: csgo/pak01_dir.vpk"); + + string dirHash = await GetHash(dirPath); + if (dirHash != dirPatch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is outdated!"); + + if (deleteOutdatedFiles) + File.Delete(dirPath); + outdated.Add(dirPatch); + needPak01Update = true; + } + else if (!validateAll) + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is up to date - will skip pak01 files"); + } + else + { + if (Debug.Enabled()) + Terminal.Debug("csgo/pak01_dir.vpk is up to date - checking all files anyway due to full validation mode"); + } + } + else + { + if (Debug.Enabled()) + Terminal.Debug("Missing: csgo/pak01_dir.vpk!"); + + missing.Add(dirPatch); + needPak01Update = true; + } + + if (!needPak01Update) + { + patches.Remove(dirPatch); + } + } + + var concurrentMissing = new ConcurrentBag(); + var concurrentOutdated = new ConcurrentBag(); + + var parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = 4 + }; + + await Parallel.ForEachAsync(patches, parallelOptions, async (patch, cancellationToken) => + { + string originalFileName = GetOriginalFileName(patch.File); + + // skip dir file (we already checked it) + if (originalFileName.Contains("pak01_dir.vpk")) + return; + + // are you a pak01 file? + bool isPak01File = originalFileName.Contains("pak01_"); + string path = Path.Combine(Directory.GetCurrentDirectory(), originalFileName); + + if (isPak01File && !needPak01Update && !validateAll) + { + if (!File.Exists(path)) + { + if (Debug.Enabled()) + Terminal.Debug($"Missing: {originalFileName}"); + + concurrentMissing.Add(patch); + return; + } + + if (Debug.Enabled()) + Terminal.Debug($"Skipping hash check for: {originalFileName} (pak01_dir.vpk up to date)"); + + return; + } + + if (!File.Exists(path)) + { + if (Debug.Enabled()) + Terminal.Debug($"Missing: {originalFileName}"); + + concurrentMissing.Add(patch); + return; + } + + if (Debug.Enabled()) + Terminal.Debug($"Checking hash for: {originalFileName}{(isPak01File && validateAll ? " (full validation)" : "")}"); + + string hash = await GetHash(path); + if (hash != patch.Hash) + { + if (Debug.Enabled()) + Terminal.Debug($"Outdated: {originalFileName}"); + + if (deleteOutdatedFiles) + File.Delete(path); + concurrentOutdated.Add(patch); + } + }); + + missing.AddRange(concurrentMissing); + outdated.AddRange(concurrentOutdated); + + // if pak01_dir.vpk needs update, move it to end of lists + if (needPak01Update && dirPatch != null) + { + if (outdated.Remove(dirPatch)) + outdated.Add(dirPatch); + if (missing.Remove(dirPatch)) + missing.Add(dirPatch); + } + } + + return new Patches(patches.Count > 0, missing, outdated); + } + } +}