diff --git a/.editorconfig b/.editorconfig index 9d66212..8c57ea5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -118,8 +118,8 @@ csharp_style_prefer_index_operator = true csharp_style_prefer_null_check_over_type_check = true csharp_style_prefer_range_operator = true csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = unused_local_variable:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:silent +csharp_style_unused_value_expression_statement_preference = discard_variable # 'using' directive preferences csharp_using_directive_placement = outside_namespace:suggestion diff --git a/CreamInstaller/Classes/ExceptionHandler.cs b/CreamInstaller/Classes/ExceptionHandler.cs index 2174887..42dc7e4 100644 --- a/CreamInstaller/Classes/ExceptionHandler.cs +++ b/CreamInstaller/Classes/ExceptionHandler.cs @@ -1,60 +1,59 @@ using System; using System.Windows.Forms; -namespace CreamInstaller +namespace CreamInstaller; + +internal static class ExceptionHandler { - internal static class ExceptionHandler + internal static bool OutputException(Exception e) { - internal static bool OutputException(Exception e) + while (e.InnerException is not null) { - while (e.InnerException is not null) - { - e = e.InnerException; - } - - string output = ""; - string[] stackTrace = e.StackTrace?.Split('\n'); - if (stackTrace is not null && stackTrace.Length > 0) - { - output += "STACK TRACE\n"; - for (int i = 0; i < Math.Min(stackTrace.Length, 3); i++) - { - string line = stackTrace[i]; - if (line is not null) - { - output += "\n " + line[line.IndexOf("at")..]; - } - } - } - string[] messageLines = e.Message?.Split('\n'); - if (messageLines is not null && messageLines.Length > 0) - { - if (output.Length > 0) - { - output += "\n\n"; - } - - output += "MESSAGE\n"; - for (int i = 0; i < messageLines.Length; i++) - { - string line = messageLines[i]; - if (line is not null) - { - output += "\n " + messageLines[i]; - } - } - } - return MessageBox.Show(output, caption: "CreamInstaller encountered an exception", buttons: MessageBoxButtons.RetryCancel, icon: MessageBoxIcon.Error) == DialogResult.Retry; + e = e.InnerException; } + + string output = ""; + string[] stackTrace = e.StackTrace?.Split('\n'); + if (stackTrace is not null && stackTrace.Length > 0) + { + output += "STACK TRACE\n"; + for (int i = 0; i < Math.Min(stackTrace.Length, 3); i++) + { + string line = stackTrace[i]; + if (line is not null) + { + output += "\n " + line[line.IndexOf("at")..]; + } + } + } + string[] messageLines = e.Message?.Split('\n'); + if (messageLines is not null && messageLines.Length > 0) + { + if (output.Length > 0) + { + output += "\n\n"; + } + + output += "MESSAGE\n"; + for (int i = 0; i < messageLines.Length; i++) + { + string line = messageLines[i]; + if (line is not null) + { + output += "\n " + messageLines[i]; + } + } + } + return MessageBox.Show(output, caption: "CreamInstaller encountered an exception", buttons: MessageBoxButtons.RetryCancel, icon: MessageBoxIcon.Error) == DialogResult.Retry; } +} - internal class CustomMessageException : Exception - { - private readonly string message; - public override string Message => message ?? "CustomMessageException"; +internal class CustomMessageException : Exception +{ + private readonly string message; + public override string Message => message ?? "CustomMessageException"; - public override string ToString() => Message; + public override string ToString() => Message; - internal CustomMessageException(string message) => this.message = message; - } -} \ No newline at end of file + internal CustomMessageException(string message) => this.message = message; +} diff --git a/CreamInstaller/Classes/InstallationLog.cs b/CreamInstaller/Classes/InstallationLog.cs index 65be8bd..9be189d 100644 --- a/CreamInstaller/Classes/InstallationLog.cs +++ b/CreamInstaller/Classes/InstallationLog.cs @@ -1,25 +1,24 @@ using System.Drawing; using System.Windows.Forms; -namespace CreamInstaller -{ - internal static class InstallationLog - { - internal static readonly Color Background = Color.DarkSlateGray; - internal static readonly Color Operation = Color.LightGray; - internal static readonly Color Resource = Color.LightBlue; - internal static readonly Color Success = Color.LightGreen; - internal static readonly Color Cleanup = Color.YellowGreen; - internal static readonly Color Warning = Color.Yellow; - internal static readonly Color Error = Color.DarkOrange; +namespace CreamInstaller; - internal static void AppendText(this RichTextBox logTextBox, string text, Color color) - { - logTextBox.SelectionStart = logTextBox.TextLength; - logTextBox.SelectionLength = 0; - logTextBox.SelectionColor = color; - logTextBox.AppendText(text); - logTextBox.SelectionColor = logTextBox.ForeColor; - } +internal static class InstallationLog +{ + internal static readonly Color Background = Color.DarkSlateGray; + internal static readonly Color Operation = Color.LightGray; + internal static readonly Color Resource = Color.LightBlue; + internal static readonly Color Success = Color.LightGreen; + internal static readonly Color Cleanup = Color.YellowGreen; + internal static readonly Color Warning = Color.Yellow; + internal static readonly Color Error = Color.DarkOrange; + + internal static void AppendText(this RichTextBox logTextBox, string text, Color color) + { + logTextBox.SelectionStart = logTextBox.TextLength; + logTextBox.SelectionLength = 0; + logTextBox.SelectionColor = color; + logTextBox.AppendText(text); + logTextBox.SelectionColor = logTextBox.ForeColor; } -} \ No newline at end of file +} diff --git a/CreamInstaller/Classes/ProgramSelection.cs b/CreamInstaller/Classes/ProgramSelection.cs index a8de90b..2092a3a 100644 --- a/CreamInstaller/Classes/ProgramSelection.cs +++ b/CreamInstaller/Classes/ProgramSelection.cs @@ -7,131 +7,130 @@ using System.Threading.Tasks; using Gameloop.Vdf.Linq; -namespace CreamInstaller +namespace CreamInstaller; + +internal class ProgramSelection { - internal class ProgramSelection + internal bool Enabled = false; + internal bool Usable = true; + + internal int SteamAppId = 0; + internal string Name = "Program"; + + internal Image Icon; + private string iconPath; + internal string IconPath { - internal bool Enabled = false; - internal bool Usable = true; - - internal int SteamAppId = 0; - internal string Name = "Program"; - - internal Image Icon; - private string iconPath; - internal string IconPath + get => iconPath; + set { - get => iconPath; - set - { - iconPath = value; - Task.Run(async () => Icon = await Program.GetImageFromUrl(iconPath)); - } - } - internal string IconStaticID - { - set => IconPath = $"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{SteamAppId}/{value}.jpg"; - } - - internal Image ClientIcon; - private string clientIconPath; - internal string ClientIconPath - { - get => clientIconPath; - set - { - clientIconPath = value; - Task.Run(async () => ClientIcon = await Program.GetImageFromUrl(clientIconPath)); - } - } - internal string ClientIconStaticID - { - set => ClientIconPath = $"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{SteamAppId}/{value}.ico"; - } - - internal string RootDirectory; - internal List SteamApiDllDirectories; - - internal VProperty AppInfo = null; - - internal readonly SortedList AllSteamDlc = new(); - internal readonly SortedList SelectedSteamDlc = new(); - internal readonly List>> ExtraSteamAppIdDlc = new(); - - internal bool AreSteamApiDllsLocked - { - get - { - foreach (string directory in SteamApiDllDirectories) - { - string api = directory + @"\steam_api.dll"; - string api64 = directory + @"\steam_api64.dll"; - if (api.IsFilePathLocked() || api64.IsFilePathLocked()) return true; - } - return false; - } - } - - private void Toggle(KeyValuePair dlcApp, bool enabled) - { - if (enabled) SelectedSteamDlc[dlcApp.Key] = dlcApp.Value; - else SelectedSteamDlc.Remove(dlcApp.Key); - } - - internal void ToggleDlc(int dlcAppId, bool enabled) - { - foreach (KeyValuePair dlcApp in AllSteamDlc) - if (dlcApp.Key == dlcAppId) - { - Toggle(dlcApp, enabled); - break; - } - Enabled = SelectedSteamDlc.Any(); - } - - internal void ToggleAllDlc(bool enabled) - { - if (!enabled) SelectedSteamDlc.Clear(); - else foreach (KeyValuePair dlcApp in AllSteamDlc) Toggle(dlcApp, enabled); - Enabled = SelectedSteamDlc.Any(); - } - - internal ProgramSelection() => All.Add(this); - - internal void Validate() - { - if (Program.BlockProtectedGames && Program.IsGameBlocked(Name, RootDirectory)) - { - All.Remove(this); - return; - } - if (!Directory.Exists(RootDirectory)) - { - All.Remove(this); - return; - } - SteamApiDllDirectories.RemoveAll(directory => !Directory.Exists(directory)); - if (!SteamApiDllDirectories.Any()) All.Remove(this); - } - - internal static void ValidateAll() => AllSafe.ForEach(selection => selection.Validate()); - - internal static List All => Program.ProgramSelections; - - internal static List AllSafe => All.ToList(); - - internal static List AllUsable => All.FindAll(s => s.Usable); - - internal static List AllUsableEnabled => AllUsable.FindAll(s => s.Enabled); - - internal static ProgramSelection FromAppId(int appId) => AllUsable.Find(s => s.SteamAppId == appId); - - internal static KeyValuePair? GetDlcFromAppId(int appId) - { - foreach (ProgramSelection selection in AllUsable) - foreach (KeyValuePair app in selection.AllSteamDlc) - if (app.Key == appId) return app; - return null; + iconPath = value; + Task.Run(async () => Icon = await Program.GetImageFromUrl(iconPath)); } } -} \ No newline at end of file + internal string IconStaticID + { + set => IconPath = $"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{SteamAppId}/{value}.jpg"; + } + + internal Image ClientIcon; + private string clientIconPath; + internal string ClientIconPath + { + get => clientIconPath; + set + { + clientIconPath = value; + Task.Run(async () => ClientIcon = await Program.GetImageFromUrl(clientIconPath)); + } + } + internal string ClientIconStaticID + { + set => ClientIconPath = $"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{SteamAppId}/{value}.ico"; + } + + internal string RootDirectory; + internal List SteamApiDllDirectories; + + internal VProperty AppInfo = null; + + internal readonly SortedList AllSteamDlc = new(); + internal readonly SortedList SelectedSteamDlc = new(); + internal readonly List>> ExtraSteamAppIdDlc = new(); + + internal bool AreSteamApiDllsLocked + { + get + { + foreach (string directory in SteamApiDllDirectories) + { + string api = directory + @"\steam_api.dll"; + string api64 = directory + @"\steam_api64.dll"; + if (api.IsFilePathLocked() || api64.IsFilePathLocked()) return true; + } + return false; + } + } + + private void Toggle(KeyValuePair dlcApp, bool enabled) + { + if (enabled) SelectedSteamDlc[dlcApp.Key] = dlcApp.Value; + else SelectedSteamDlc.Remove(dlcApp.Key); + } + + internal void ToggleDlc(int dlcAppId, bool enabled) + { + foreach (KeyValuePair dlcApp in AllSteamDlc) + if (dlcApp.Key == dlcAppId) + { + Toggle(dlcApp, enabled); + break; + } + Enabled = SelectedSteamDlc.Any(); + } + + internal void ToggleAllDlc(bool enabled) + { + if (!enabled) SelectedSteamDlc.Clear(); + else foreach (KeyValuePair dlcApp in AllSteamDlc) Toggle(dlcApp, enabled); + Enabled = SelectedSteamDlc.Any(); + } + + internal ProgramSelection() => All.Add(this); + + internal void Validate() + { + if (Program.BlockProtectedGames && Program.IsGameBlocked(Name, RootDirectory)) + { + All.Remove(this); + return; + } + if (!Directory.Exists(RootDirectory)) + { + All.Remove(this); + return; + } + SteamApiDllDirectories.RemoveAll(directory => !Directory.Exists(directory)); + if (!SteamApiDllDirectories.Any()) All.Remove(this); + } + + internal static void ValidateAll() => AllSafe.ForEach(selection => selection.Validate()); + + internal static List All => Program.ProgramSelections; + + internal static List AllSafe => All.ToList(); + + internal static List AllUsable => All.FindAll(s => s.Usable); + + internal static List AllUsableEnabled => AllUsable.FindAll(s => s.Enabled); + + internal static ProgramSelection FromAppId(int appId) => AllUsable.Find(s => s.SteamAppId == appId); + + internal static KeyValuePair? GetDlcFromAppId(int appId) + { + foreach (ProgramSelection selection in AllUsable) + foreach (KeyValuePair app in selection.AllSteamDlc) + if (app.Key == appId) return app; + return null; + } +} diff --git a/CreamInstaller/Classes/SteamCMD.cs b/CreamInstaller/Classes/SteamCMD.cs index 44436ab..f82b809 100644 --- a/CreamInstaller/Classes/SteamCMD.cs +++ b/CreamInstaller/Classes/SteamCMD.cs @@ -11,157 +11,158 @@ using System.Windows.Forms; using Gameloop.Vdf.Linq; -namespace CreamInstaller +namespace CreamInstaller; + +internal static class SteamCMD { - internal static class SteamCMD + internal static readonly string DirectoryPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\CreamInstaller"; + internal static readonly string FilePath = DirectoryPath + @"\steamcmd.exe"; + internal static readonly string ArchivePath = DirectoryPath + @"\steamcmd.zip"; + internal static readonly string DllPath = DirectoryPath + @"\steamclient.dll"; + internal static readonly string AppCachePath = DirectoryPath + @"\appcache"; + internal static readonly string AppCacheAppInfoPath = AppCachePath + @"\appinfo.vdf"; + internal static readonly string AppInfoPath = DirectoryPath + @"\appinfo"; + + internal static readonly Version MinimumAppInfoVersion = Version.Parse("2.0.3.2"); + internal static readonly string AppInfoVersionPath = AppInfoPath + @"\version.txt"; + + internal static async Task Run(string command) => await Task.Run(() => { - internal static readonly string DirectoryPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\CreamInstaller"; - internal static readonly string FilePath = DirectoryPath + @"\steamcmd.exe"; - internal static readonly string ArchivePath = DirectoryPath + @"\steamcmd.zip"; - internal static readonly string DllPath = DirectoryPath + @"\steamclient.dll"; - internal static readonly string AppCachePath = DirectoryPath + @"\appcache"; - internal static readonly string AppCacheAppInfoPath = AppCachePath + @"\appinfo.vdf"; - internal static readonly string AppInfoPath = DirectoryPath + @"\appinfo"; - - internal static readonly Version MinimumAppInfoVersion = Version.Parse("2.0.3.2"); - internal static readonly string AppInfoVersionPath = AppInfoPath + @"\version.txt"; - - internal static async Task Run(string command) => await Task.Run(() => + if (Program.Canceled) return ""; + List logs = new(); + ProcessStartInfo processStartInfo = new() { - if (Program.Canceled) return ""; - List logs = new(); - ProcessStartInfo processStartInfo = new() - { - FileName = FilePath, - RedirectStandardOutput = true, - RedirectStandardInput = true, - RedirectStandardError = true, - UseShellExecute = false, - Arguments = command, - CreateNoWindow = true, - StandardInputEncoding = Encoding.UTF8, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - using Process process = Process.Start(processStartInfo); - process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => logs.Add(e.Data); - process.BeginOutputReadLine(); - process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => logs.Add(e.Data); - process.BeginErrorReadLine(); - process.WaitForExit(); - return string.Join("\r\n", logs); - }); + FileName = FilePath, + RedirectStandardOutput = true, + RedirectStandardInput = true, + RedirectStandardError = true, + UseShellExecute = false, + Arguments = command, + CreateNoWindow = true, + StandardInputEncoding = Encoding.UTF8, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + using Process process = Process.Start(processStartInfo); + process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => logs.Add(e.Data); + process.BeginOutputReadLine(); + process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) => logs.Add(e.Data); + process.BeginErrorReadLine(); + process.WaitForExit(); + return string.Join("\r\n", logs); + }); - internal static async Task Setup() + internal static async Task Setup() + { + await Kill(); + if (!File.Exists(FilePath)) { - await Kill(); - if (!File.Exists(FilePath)) + using (HttpClient httpClient = new()) { - using (HttpClient httpClient = new()) - { - byte[] file = await httpClient.GetByteArrayAsync("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"); - file.Write(ArchivePath); - } - ZipFile.ExtractToDirectory(ArchivePath, DirectoryPath); - File.Delete(ArchivePath); + byte[] file = await httpClient.GetByteArrayAsync("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"); + file.Write(ArchivePath); } - if (File.Exists(AppCacheAppInfoPath)) File.Delete(AppCacheAppInfoPath); - if (!File.Exists(AppInfoVersionPath) || !Version.TryParse(File.ReadAllText(AppInfoVersionPath, Encoding.UTF8), out Version version) || version < MinimumAppInfoVersion) - { - if (Directory.Exists(AppInfoPath)) Directory.Delete(AppInfoPath, true); - Directory.CreateDirectory(AppInfoPath); - File.WriteAllText(AppInfoVersionPath, Application.ProductVersion, Encoding.UTF8); - } - if (!File.Exists(DllPath)) await Run($@"+quit"); + ZipFile.ExtractToDirectory(ArchivePath, DirectoryPath); + File.Delete(ArchivePath); } - - internal static async Task GetAppInfo(int appId, string branch = "public", int buildId = 0) + if (File.Exists(AppCacheAppInfoPath)) File.Delete(AppCacheAppInfoPath); + if (!File.Exists(AppInfoVersionPath) || !Version.TryParse(File.ReadAllText(AppInfoVersionPath, Encoding.UTF8), out Version version) || version < MinimumAppInfoVersion) { - if (Program.Canceled) return null; - string output; - string appUpdatePath = $@"{AppInfoPath}\{appId}"; - string appUpdateFile = $@"{appUpdatePath}\appinfo.txt"; - restart: - if (Program.Canceled) return null; - if (Directory.Exists(appUpdatePath) && File.Exists(appUpdateFile)) - output = File.ReadAllText(appUpdateFile, Encoding.UTF8); - else - { - output = await Run($@"+@ShutdownOnFailedCommand 0 +login anonymous +app_info_print {appId} +force_install_dir {appUpdatePath} +app_update 4 +quit"); - int openBracket = output.IndexOf("{"); - int closeBracket = output.LastIndexOf("}"); - if (openBracket != -1 && closeBracket != -1) - { - output = $"\"{appId}\"\n" + output[openBracket..(1 + closeBracket)]; - File.WriteAllText(appUpdateFile, output, Encoding.UTF8); - } - } - if (Program.Canceled || output is null) return null; - if (!ValveDataFile.TryDeserialize(output, out VProperty appInfo)) - { - if (Directory.Exists(appUpdatePath)) - { - Directory.Delete(appUpdatePath, true); - goto restart; - } - } - if (appInfo.Value is VValue) goto restart; - if (appInfo is null || appInfo.Value?.Children()?.ToList()?.Count == 0) return appInfo; - VToken type = appInfo.Value?.TryGet("common")?.TryGet("type"); - if (type is null || type.ToString() == "Game") - { - string buildid = appInfo.Value?.TryGet("depots")?.TryGet("branches")?.TryGet(branch)?.TryGet("buildid")?.ToString(); - if (buildid is null && type is not null) return appInfo; - if (type is null || int.Parse(buildid) < buildId) - { - List dlcAppIds = await ParseDlcAppIds(appInfo); - foreach (int id in dlcAppIds) - { - string dlcAppUpdatePath = $@"{AppInfoPath}\{id}"; - if (Directory.Exists(dlcAppUpdatePath)) Directory.Delete(dlcAppUpdatePath, true); - } - if (Directory.Exists(appUpdatePath)) Directory.Delete(appUpdatePath, true); - goto restart; - } - } - return appInfo; + if (Directory.Exists(AppInfoPath)) Directory.Delete(AppInfoPath, true); + Directory.CreateDirectory(AppInfoPath); + File.WriteAllText(AppInfoVersionPath, Application.ProductVersion, Encoding.UTF8); } + if (!File.Exists(DllPath)) await Run($@"+quit"); + } - internal static async Task> ParseDlcAppIds(VProperty appInfo) => await Task.Run(() => + internal static async Task GetAppInfo(int appId, string branch = "public", int buildId = 0) + { + if (Program.Canceled) return null; + string output; + string appUpdatePath = $@"{AppInfoPath}\{appId}"; + string appUpdateFile = $@"{appUpdatePath}\appinfo.txt"; + restart: + if (Program.Canceled) return null; + if (Directory.Exists(appUpdatePath) && File.Exists(appUpdateFile)) + output = File.ReadAllText(appUpdateFile, Encoding.UTF8); + else { - List dlcIds = new(); - if (Program.Canceled || appInfo is not VProperty) return dlcIds; - VToken extended = appInfo.Value.TryGet("extended"); - if (extended is not null) foreach (VProperty property in extended) - if (property.Key.ToString() == "listofdlc") foreach (string id in property.Value.ToString().Split(",")) - if (!dlcIds.Contains(int.Parse(id))) dlcIds.Add(int.Parse(id)); - VToken depots = appInfo.Value.TryGet("depots"); - if (depots is not null) foreach (VProperty property in depots) - if (int.TryParse(property.Key.ToString(), out int _) - && int.TryParse(property.Value.TryGet("dlcappid")?.ToString(), out int appid) - && !dlcIds.Contains(appid)) - dlcIds.Add(appid); - return dlcIds; - }); - - internal static async Task Kill() - { - List tasks = new(); - foreach (Process process in Process.GetProcessesByName("steamcmd")) + output = await Run($@"+@ShutdownOnFailedCommand 0 +login anonymous +app_info_print {appId} +force_install_dir {appUpdatePath} +app_update 4 +quit"); + int openBracket = output.IndexOf("{"); + int closeBracket = output.LastIndexOf("}"); + if (openBracket != -1 && closeBracket != -1) { - process.Kill(); - tasks.Add(Task.Run(() => process.WaitForExit())); + output = $"\"{appId}\"\n" + output[openBracket..(1 + closeBracket)]; + File.WriteAllText(appUpdateFile, output, Encoding.UTF8); } - foreach (Task task in tasks) await task; } - - internal static void Dispose() + if (Program.Canceled || output is null) return null; + if (!ValveDataFile.TryDeserialize(output, out VProperty appInfo)) { - Kill().Wait(); - if (Directory.Exists(DirectoryPath)) + if (Directory.Exists(appUpdatePath)) { - Directory.Delete(DirectoryPath, true); + Directory.Delete(appUpdatePath, true); + goto restart; } } + if (appInfo.Value is VValue) goto restart; + if (appInfo is null || appInfo.Value?.Children()?.ToList()?.Count == 0) return appInfo; + VToken type = appInfo.Value?.GetChild("common")?.GetChild("type"); + if (type is null || type.ToString() == "Game") + { + string buildid = appInfo.Value?.GetChild("depots")?.GetChild("branches")?.GetChild(branch)?.GetChild("buildid")?.ToString(); + if (buildid is null && type is not null) return appInfo; + if (type is null || int.Parse(buildid) < buildId) + { + List dlcAppIds = await ParseDlcAppIds(appInfo); + foreach (int id in dlcAppIds) + { + string dlcAppUpdatePath = $@"{AppInfoPath}\{id}"; + if (Directory.Exists(dlcAppUpdatePath)) Directory.Delete(dlcAppUpdatePath, true); + } + if (Directory.Exists(appUpdatePath)) Directory.Delete(appUpdatePath, true); + goto restart; + } + } + return appInfo; + } + + internal static async Task> ParseDlcAppIds(VProperty appInfo) => await Task.Run(() => + { + List dlcIds = new(); + if (Program.Canceled || appInfo is not VProperty) return dlcIds; + VToken extended = appInfo.Value.GetChild("extended"); + if (extended is not null) + foreach (VProperty property in extended) + if (property.Key.ToString() == "listofdlc") + foreach (string id in property.Value.ToString().Split(",")) + if (!dlcIds.Contains(int.Parse(id))) dlcIds.Add(int.Parse(id)); + VToken depots = appInfo.Value.GetChild("depots"); + if (depots is not null) foreach (VProperty property in depots) + if (int.TryParse(property.Key.ToString(), out int _) + && int.TryParse(property.Value.GetChild("dlcappid")?.ToString(), out int appid) + && !dlcIds.Contains(appid)) + dlcIds.Add(appid); + return dlcIds; + }); + + internal static async Task Kill() + { + List tasks = new(); + foreach (Process process in Process.GetProcessesByName("steamcmd")) + { + process.Kill(); + tasks.Add(Task.Run(() => process.WaitForExit())); + } + foreach (Task task in tasks) await task; + } + + internal static void Dispose() + { + Kill().Wait(); + if (Directory.Exists(DirectoryPath)) + { + Directory.Delete(DirectoryPath, true); + } } -} \ No newline at end of file +} diff --git a/CreamInstaller/Classes/ValveDataFile.cs b/CreamInstaller/Classes/ValveDataFile.cs index 635be12..cbee8b2 100644 --- a/CreamInstaller/Classes/ValveDataFile.cs +++ b/CreamInstaller/Classes/ValveDataFile.cs @@ -17,7 +17,7 @@ internal static class ValveDataFile return false; } - internal static VToken TryGet(this VToken token, string index) + internal static VToken GetChild(this VToken token, string index) { try { diff --git a/CreamInstaller/CreamInstaller.csproj b/CreamInstaller/CreamInstaller.csproj index 5de2530..dc4bd4d 100644 --- a/CreamInstaller/CreamInstaller.csproj +++ b/CreamInstaller/CreamInstaller.csproj @@ -5,7 +5,7 @@ true Resources\ini.ico true - 2.2.4.1 + 2.2.4.2 Resources\ini.ico Automatically generates and installs CreamAPI files for Steam games on the user's computer. It can also generate and install CreamAPI for the Paradox Launcher should the user select a Paradox Interactive game. diff --git a/CreamInstaller/Forms/Components/CustomForm.cs b/CreamInstaller/Forms/Components/CustomForm.cs index ca2fd38..748a77a 100644 --- a/CreamInstaller/Forms/Components/CustomForm.cs +++ b/CreamInstaller/Forms/Components/CustomForm.cs @@ -1,21 +1,20 @@ using System.Windows.Forms; -namespace CreamInstaller +namespace CreamInstaller; + +internal class CustomForm : Form { - internal class CustomForm : Form + internal CustomForm() : base() => Icon = Properties.Resources.Icon; + + internal CustomForm(IWin32Window owner) : this() => Owner = owner as Form; + + protected override CreateParams CreateParams // Double buffering for all controls { - internal CustomForm() : base() => Icon = Properties.Resources.Icon; - - internal CustomForm(IWin32Window owner) : this() => Owner = owner as Form; - - protected override CreateParams CreateParams // Double buffering for all controls + get { - get - { - CreateParams handleParam = base.CreateParams; - handleParam.ExStyle |= 0x02000000; // WS_EX_COMPOSITED - return handleParam; - } + CreateParams handleParam = base.CreateParams; + handleParam.ExStyle |= 0x02000000; // WS_EX_COMPOSITED + return handleParam; } } } diff --git a/CreamInstaller/Forms/Components/CustomTreeView.cs b/CreamInstaller/Forms/Components/CustomTreeView.cs index 14e2d91..ed5a591 100644 --- a/CreamInstaller/Forms/Components/CustomTreeView.cs +++ b/CreamInstaller/Forms/Components/CustomTreeView.cs @@ -2,56 +2,55 @@ using System.Drawing; using System.Windows.Forms; -namespace CreamInstaller +namespace CreamInstaller; + +internal class CustomTreeView : TreeView { - internal class CustomTreeView : TreeView + protected override void WndProc(ref Message m) { - protected override void WndProc(ref Message m) + if (m.Msg == 0x203) { - if (m.Msg == 0x203) - { - m.Result = IntPtr.Zero; - } - else - { - base.WndProc(ref m); - } + m.Result = IntPtr.Zero; } - - internal CustomTreeView() : base() + else { - DrawMode = TreeViewDrawMode.OwnerDrawAll; - DrawNode += new DrawTreeNodeEventHandler(DrawTreeNode); - } - - private void DrawTreeNode(object sender, DrawTreeNodeEventArgs e) - { - e.DrawDefault = true; - TreeNode node = e.Node; - if (!node.IsVisible) - { - return; - } - - Graphics graphics = e.Graphics; - Color backColor = BackColor; - SolidBrush brush = new(backColor); - Font font = Font; - Font subFont = new(font.FontFamily, font.SizeInPoints, FontStyle.Regular, font.Unit, font.GdiCharSet, font.GdiVerticalFont); - - string subText = node.Name; - if (subText is null || !int.TryParse(subText, out int subInt) || subInt <= 0) - { - return; - } - - Size subSize = TextRenderer.MeasureText(graphics, subText, subFont); - Rectangle bounds = node.Bounds; - Rectangle subBounds = new(bounds.X + bounds.Width, bounds.Y, subSize.Width, bounds.Height); - graphics.FillRectangle(brush, subBounds); - Point location = subBounds.Location; - Point subLocation = new(location.X - 1, location.Y + 1); - TextRenderer.DrawText(graphics, subText, subFont, subLocation, Color.Gray); + base.WndProc(ref m); } } -} \ No newline at end of file + + internal CustomTreeView() : base() + { + DrawMode = TreeViewDrawMode.OwnerDrawAll; + DrawNode += new DrawTreeNodeEventHandler(DrawTreeNode); + } + + private void DrawTreeNode(object sender, DrawTreeNodeEventArgs e) + { + e.DrawDefault = true; + TreeNode node = e.Node; + if (!node.IsVisible) + { + return; + } + + Graphics graphics = e.Graphics; + Color backColor = BackColor; + SolidBrush brush = new(backColor); + Font font = Font; + Font subFont = new(font.FontFamily, font.SizeInPoints, FontStyle.Regular, font.Unit, font.GdiCharSet, font.GdiVerticalFont); + + string subText = node.Name; + if (subText is null || !int.TryParse(subText, out int subInt) || subInt <= 0) + { + return; + } + + Size subSize = TextRenderer.MeasureText(graphics, subText, subFont); + Rectangle bounds = node.Bounds; + Rectangle subBounds = new(bounds.X + bounds.Width, bounds.Y, subSize.Width, bounds.Height); + graphics.FillRectangle(brush, subBounds); + Point location = subBounds.Location; + Point subLocation = new(location.X - 1, location.Y + 1); + TextRenderer.DrawText(graphics, subText, subFont, subLocation, Color.Gray); + } +} diff --git a/CreamInstaller/Forms/DialogForm.cs b/CreamInstaller/Forms/DialogForm.cs index e240809..1f14452 100644 --- a/CreamInstaller/Forms/DialogForm.cs +++ b/CreamInstaller/Forms/DialogForm.cs @@ -1,25 +1,24 @@ using System.Drawing; using System.Windows.Forms; -namespace CreamInstaller -{ - internal partial class DialogForm : CustomForm - { - internal DialogForm(IWin32Window owner) : base(owner) => InitializeComponent(); +namespace CreamInstaller; - internal DialogResult Show(string formName, Icon descriptionIcon, string descriptionText, string acceptButtonText, string cancelButtonText = null) +internal partial class DialogForm : CustomForm +{ + internal DialogForm(IWin32Window owner) : base(owner) => InitializeComponent(); + + internal DialogResult Show(string formName, Icon descriptionIcon, string descriptionText, string acceptButtonText, string cancelButtonText = null) + { + icon.Image = descriptionIcon.ToBitmap(); + Text = formName; + descriptionLabel.Text = descriptionText; + acceptButton.Text = acceptButtonText; + if (cancelButtonText is null) { - icon.Image = descriptionIcon.ToBitmap(); - Text = formName; - descriptionLabel.Text = descriptionText; - acceptButton.Text = acceptButtonText; - if (cancelButtonText is null) - { - cancelButton.Enabled = false; - cancelButton.Visible = false; - } - else cancelButton.Text = cancelButtonText; - return ShowDialog(); + cancelButton.Enabled = false; + cancelButton.Visible = false; } + else cancelButton.Text = cancelButtonText; + return ShowDialog(); } -} \ No newline at end of file +} diff --git a/CreamInstaller/Forms/InstallForm.cs b/CreamInstaller/Forms/InstallForm.cs index 0ee66a6..4a9b9d8 100644 --- a/CreamInstaller/Forms/InstallForm.cs +++ b/CreamInstaller/Forms/InstallForm.cs @@ -8,236 +8,227 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -namespace CreamInstaller +namespace CreamInstaller; + +internal partial class InstallForm : CustomForm { - internal partial class InstallForm : CustomForm + internal bool Reselecting = false; + internal bool Uninstalling = false; + + internal InstallForm(IWin32Window owner, bool uninstall = false) : base(owner) { - internal bool Reselecting = false; - internal bool Uninstalling = false; + InitializeComponent(); + Text = Program.ApplicationName; + Program.InstallForm = this; + logTextBox.BackColor = InstallationLog.Background; + Uninstalling = uninstall; + } - internal InstallForm(IWin32Window owner, bool uninstall = false) : base(owner) + private int OperationsCount; + private int CompleteOperationsCount; + + internal void UpdateProgress(int progress) + { + int value = (int)((float)(CompleteOperationsCount / (float)OperationsCount) * 100) + (progress / OperationsCount); + if (value < userProgressBar.Value) return; + userProgressBar.Value = value; + } + + internal async Task UpdateUser(string text, Color color, bool info = true, bool log = true) + { + if (info) userInfoLabel.Text = text; + if (log && !logTextBox.IsDisposed) { - InitializeComponent(); - Text = Program.ApplicationName; - Program.InstallForm = this; - logTextBox.BackColor = InstallationLog.Background; - Uninstalling = uninstall; + if (logTextBox.Text.Length > 0) logTextBox.AppendText(Environment.NewLine, color); + logTextBox.AppendText(text, color); } + await Task.Run(() => Thread.Sleep(0)); // to keep the text box control from glitching + } - private int OperationsCount; - private int CompleteOperationsCount; - - internal void UpdateProgress(int progress) + internal async Task WriteConfiguration(StreamWriter writer, int steamAppId, string name, SortedList steamDlcApps) + { + writer.WriteLine(); + writer.WriteLine($"; {name}"); + writer.WriteLine("[steam]"); + writer.WriteLine($"appid = {steamAppId}"); + writer.WriteLine(); + writer.WriteLine("[dlc]"); + await UpdateUser($"Added game to cream_api.ini with appid {steamAppId} ({name})", InstallationLog.Resource, info: false); + foreach (KeyValuePair dlcApp in steamDlcApps) { - int value = (int)((float)(CompleteOperationsCount / (float)OperationsCount) * 100) + (progress / OperationsCount); - if (value < userProgressBar.Value) return; - userProgressBar.Value = value; + writer.WriteLine($"{dlcApp.Key} = {dlcApp.Value}"); + await UpdateUser($"Added DLC to cream_api.ini with appid {dlcApp.Key} ({dlcApp.Value})", InstallationLog.Resource, info: false); } + } - internal async Task UpdateUser(string text, Color color, bool info = true, bool log = true) + private async Task OperateFor(ProgramSelection selection) + { + UpdateProgress(0); + int count = selection.SteamApiDllDirectories.Count; + int cur = 0; + foreach (string directory in selection.SteamApiDllDirectories) { - if (info) userInfoLabel.Text = text; - if (log && !logTextBox.IsDisposed) + await UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); + if (!Program.IsProgramRunningDialog(this, selection)) throw new OperationCanceledException(); + string api = directory + @"\steam_api.dll"; + string api_o = directory + @"\steam_api_o.dll"; + string api64 = directory + @"\steam_api64.dll"; + string api64_o = directory + @"\steam_api64_o.dll"; + string cApi = directory + @"\cream_api.ini"; + if (Uninstalling) { - if (logTextBox.Text.Length > 0) logTextBox.AppendText(Environment.NewLine, color); - logTextBox.AppendText(text, color); + if (File.Exists(api_o)) + { + if (File.Exists(api)) + { + File.Delete(api); + await UpdateUser($"Deleted file: {Path.GetFileName(api)}", InstallationLog.Resource, info: false); + } + File.Move(api_o, api); + await UpdateUser($"Renamed file: {Path.GetFileName(api_o)} -> {Path.GetFileName(api)}", InstallationLog.Resource, info: false); + } + if (File.Exists(api64_o)) + { + if (File.Exists(api64)) + { + File.Delete(api64); + await UpdateUser($"Deleted file: {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); + } + File.Move(api64_o, api64); + await UpdateUser($"Renamed file: {Path.GetFileName(api64_o)} -> {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); + } + if (File.Exists(cApi)) + { + File.Delete(cApi); + await UpdateUser($"Deleted file: {Path.GetFileName(cApi)}", InstallationLog.Resource, info: false); + } } - await Task.Run(() => Thread.Sleep(0)); // to keep the text box control from glitching - } - - internal async Task WriteConfiguration(StreamWriter writer, int steamAppId, string name, SortedList steamDlcApps) - { - writer.WriteLine(); - writer.WriteLine($"; {name}"); - writer.WriteLine("[steam]"); - writer.WriteLine($"appid = {steamAppId}"); - writer.WriteLine(); - writer.WriteLine("[dlc]"); - await UpdateUser($"Added game to cream_api.ini with appid {steamAppId} ({name})", InstallationLog.Resource, info: false); - foreach (KeyValuePair dlcApp in steamDlcApps) + else { - writer.WriteLine($"{dlcApp.Key} = {dlcApp.Value}"); - await UpdateUser($"Added DLC to cream_api.ini with appid {dlcApp.Key} ({dlcApp.Value})", InstallationLog.Resource, info: false); + if (File.Exists(api) && !File.Exists(api_o)) + { + File.Move(api, api_o); + await UpdateUser($"Renamed file: {Path.GetFileName(api)} -> {Path.GetFileName(api_o)}", InstallationLog.Resource, info: false); + } + if (File.Exists(api_o)) + { + Properties.Resources.API.Write(api); + await UpdateUser($"Wrote resource to file: {Path.GetFileName(api)}", InstallationLog.Resource, info: false); + } + if (File.Exists(api64) && !File.Exists(api64_o)) + { + File.Move(api64, api64_o); + await UpdateUser($"Renamed file: {Path.GetFileName(api64)} -> {Path.GetFileName(api64_o)}", InstallationLog.Resource, info: false); + } + if (File.Exists(api64_o)) + { + Properties.Resources.API64.Write(api64); + await UpdateUser($"Wrote resource to file: {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); + } + await UpdateUser("Generating CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); + File.Create(cApi).Close(); + StreamWriter writer = new(cApi, true, Encoding.UTF8); + writer.WriteLine("; " + Application.CompanyName + " v" + Application.ProductVersion); + if (selection.SteamAppId > 0) await WriteConfiguration(writer, selection.SteamAppId, selection.Name, selection.SelectedSteamDlc); + foreach (Tuple> extraAppDlc in selection.ExtraSteamAppIdDlc) + await WriteConfiguration(writer, extraAppDlc.Item1, extraAppDlc.Item2, extraAppDlc.Item3); + writer.Flush(); + writer.Close(); } + UpdateProgress(++cur / count * 100); } + UpdateProgress(100); + } - private async Task OperateFor(ProgramSelection selection) + private async Task Operate() + { + List programSelections = ProgramSelection.AllUsableEnabled; + OperationsCount = programSelections.Count; + CompleteOperationsCount = 0; + List disabledSelections = new(); + foreach (ProgramSelection selection in programSelections) { - UpdateProgress(0); - int count = selection.SteamApiDllDirectories.Count; - int cur = 0; - foreach (string directory in selection.SteamApiDllDirectories) - { - await UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); - if (!Program.IsProgramRunningDialog(this, selection)) - { - throw new OperationCanceledException(); - } - string api = directory + @"\steam_api.dll"; - string api_o = directory + @"\steam_api_o.dll"; - string api64 = directory + @"\steam_api64.dll"; - string api64_o = directory + @"\steam_api64_o.dll"; - string cApi = directory + @"\cream_api.ini"; - if (Uninstalling) - { - if (File.Exists(api_o)) - { - if (File.Exists(api)) - { - File.Delete(api); - await UpdateUser($"Deleted file: {Path.GetFileName(api)}", InstallationLog.Resource, info: false); - } - File.Move(api_o, api); - await UpdateUser($"Renamed file: {Path.GetFileName(api_o)} -> {Path.GetFileName(api)}", InstallationLog.Resource, info: false); - } - if (File.Exists(api64_o)) - { - if (File.Exists(api64)) - { - File.Delete(api64); - await UpdateUser($"Deleted file: {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); - } - File.Move(api64_o, api64); - await UpdateUser($"Renamed file: {Path.GetFileName(api64_o)} -> {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); - } - if (File.Exists(cApi)) - { - File.Delete(cApi); - await UpdateUser($"Deleted file: {Path.GetFileName(cApi)}", InstallationLog.Resource, info: false); - } - } - else - { - if (File.Exists(api) && !File.Exists(api_o)) - { - File.Move(api, api_o); - await UpdateUser($"Renamed file: {Path.GetFileName(api)} -> {Path.GetFileName(api_o)}", InstallationLog.Resource, info: false); - } - if (File.Exists(api_o)) - { - Properties.Resources.API.Write(api); - await UpdateUser($"Wrote resource to file: {Path.GetFileName(api)}", InstallationLog.Resource, info: false); - } - if (File.Exists(api64) && !File.Exists(api64_o)) - { - File.Move(api64, api64_o); - await UpdateUser($"Renamed file: {Path.GetFileName(api64)} -> {Path.GetFileName(api64_o)}", InstallationLog.Resource, info: false); - } - if (File.Exists(api64_o)) - { - Properties.Resources.API64.Write(api64); - await UpdateUser($"Wrote resource to file: {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); - } - await UpdateUser("Generating CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); - File.Create(cApi).Close(); - StreamWriter writer = new(cApi, true, Encoding.UTF8); - writer.WriteLine("; " + Application.CompanyName + " v" + Application.ProductVersion); - if (selection.SteamAppId > 0) - { - await WriteConfiguration(writer, selection.SteamAppId, selection.Name, selection.SelectedSteamDlc); - } - foreach (Tuple> extraAppDlc in selection.ExtraSteamAppIdDlc) - { - await WriteConfiguration(writer, extraAppDlc.Item1, extraAppDlc.Item2, extraAppDlc.Item3); - } - writer.Flush(); - writer.Close(); - } - UpdateProgress(++cur / count * 100); - } - UpdateProgress(100); - } - - private async Task Operate() - { - List programSelections = ProgramSelection.AllUsableEnabled; - OperationsCount = programSelections.Count; - CompleteOperationsCount = 0; - List disabledSelections = new(); - foreach (ProgramSelection selection in programSelections) - { - if (!Program.IsProgramRunningDialog(this, selection)) throw new OperationCanceledException(); - try - { - await OperateFor(selection); - await UpdateUser($"Operation succeeded for {selection.Name}.", InstallationLog.Success); - selection.Enabled = false; - disabledSelections.Add(selection); - } - catch (Exception exception) - { - await UpdateUser($"Operation failed for {selection.Name}: " + exception.ToString(), InstallationLog.Error); - } - ++CompleteOperationsCount; - } - Program.Cleanup(); - List FailedSelections = ProgramSelection.AllUsableEnabled; - if (FailedSelections.Any()) - if (FailedSelections.Count == 1) throw new CustomMessageException($"Operation failed for {FailedSelections.First().Name}."); - else throw new CustomMessageException($"Operation failed for {FailedSelections.Count} programs."); - foreach (ProgramSelection selection in disabledSelections) selection.Enabled = true; - } - - private readonly int ProgramCount = ProgramSelection.AllUsableEnabled.Count; - - private async void Start() - { - acceptButton.Enabled = false; - retryButton.Enabled = false; - cancelButton.Enabled = true; - reselectButton.Enabled = false; - userProgressBar.Value = userProgressBar.Minimum; + if (!Program.IsProgramRunningDialog(this, selection)) throw new OperationCanceledException(); try { - await Operate(); - await UpdateUser($"CreamAPI successfully {(Uninstalling ? "uninstalled" : "installed and generated")} for " + ProgramCount + " program(s).", InstallationLog.Success); + await OperateFor(selection); + await UpdateUser($"Operation succeeded for {selection.Name}.", InstallationLog.Success); + selection.Enabled = false; + disabledSelections.Add(selection); } catch (Exception exception) { - await UpdateUser($"CreamAPI {(Uninstalling ? "uninstallation" : "installation and/or generation")} failed: " + exception.ToString(), InstallationLog.Error); - retryButton.Enabled = true; + await UpdateUser($"Operation failed for {selection.Name}: " + exception.ToString(), InstallationLog.Error); } - userProgressBar.Value = userProgressBar.Maximum; - acceptButton.Enabled = true; - cancelButton.Enabled = false; - reselectButton.Enabled = true; + ++CompleteOperationsCount; } + Program.Cleanup(); + List FailedSelections = ProgramSelection.AllUsableEnabled; + if (FailedSelections.Any()) + if (FailedSelections.Count == 1) throw new CustomMessageException($"Operation failed for {FailedSelections.First().Name}."); + else throw new CustomMessageException($"Operation failed for {FailedSelections.Count} programs."); + foreach (ProgramSelection selection in disabledSelections) selection.Enabled = true; + } - private void OnLoad(object sender, EventArgs _) + private readonly int ProgramCount = ProgramSelection.AllUsableEnabled.Count; + + private async void Start() + { + acceptButton.Enabled = false; + retryButton.Enabled = false; + cancelButton.Enabled = true; + reselectButton.Enabled = false; + userProgressBar.Value = userProgressBar.Minimum; + try { - retry: - try - { - userInfoLabel.Text = "Loading . . . "; - logTextBox.Text = string.Empty; - Start(); - } - catch (Exception e) - { - if (ExceptionHandler.OutputException(e)) goto retry; - Close(); - } + await Operate(); + await UpdateUser($"CreamAPI successfully {(Uninstalling ? "uninstalled" : "installed and generated")} for " + ProgramCount + " program(s).", InstallationLog.Success); } - - private void OnAccept(object sender, EventArgs e) + catch (Exception exception) { - Program.Cleanup(); - Close(); + await UpdateUser($"CreamAPI {(Uninstalling ? "uninstallation" : "installation and/or generation")} failed: " + exception.ToString(), InstallationLog.Error); + retryButton.Enabled = true; } + userProgressBar.Value = userProgressBar.Maximum; + acceptButton.Enabled = true; + cancelButton.Enabled = false; + reselectButton.Enabled = true; + } - private void OnRetry(object sender, EventArgs e) + private void OnLoad(object sender, EventArgs _) + { + retry: + try { - Program.Cleanup(); + userInfoLabel.Text = "Loading . . . "; + logTextBox.Text = string.Empty; Start(); } - - private void OnCancel(object sender, EventArgs e) => Program.Cleanup(); - - private void OnReselect(object sender, EventArgs e) + catch (Exception e) { - Program.Cleanup(); - Reselecting = true; + if (ExceptionHandler.OutputException(e)) goto retry; Close(); } } -} \ No newline at end of file + + private void OnAccept(object sender, EventArgs e) + { + Program.Cleanup(); + Close(); + } + + private void OnRetry(object sender, EventArgs e) + { + Program.Cleanup(); + Start(); + } + + private void OnCancel(object sender, EventArgs e) => Program.Cleanup(); + + private void OnReselect(object sender, EventArgs e) + { + Program.Cleanup(); + Reselecting = true; + Close(); + } +} diff --git a/CreamInstaller/Forms/MainForm.cs b/CreamInstaller/Forms/MainForm.cs index cd43309..7908828 100644 --- a/CreamInstaller/Forms/MainForm.cs +++ b/CreamInstaller/Forms/MainForm.cs @@ -15,189 +15,188 @@ using Onova; using Onova.Models; using Onova.Services; -namespace CreamInstaller +namespace CreamInstaller; + +internal partial class MainForm : CustomForm { - internal partial class MainForm : CustomForm + internal MainForm() : base() { - internal MainForm() : base() + InitializeComponent(); + Text = Program.ApplicationName; + } + + private static CancellationTokenSource cancellationTokenSource; + + private void StartProgram() + { + if (cancellationTokenSource is not null) { - InitializeComponent(); - Text = Program.ApplicationName; + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + cancellationTokenSource = null; } + Hide(); + new SelectForm(this).ShowDialog(); + Close(); + } - private static CancellationTokenSource cancellationTokenSource; + private static readonly HttpClient httpClient = new(); + private static UpdateManager updateManager = null; + private static Version latestVersion = null; + private static IReadOnlyList versions; - private void StartProgram() + private async void OnLoad() + { + Size = new(420, 85); + progressBar1.Visible = false; + ignoreButton.Visible = true; + updateButton.Text = "Update"; + updateButton.Click -= OnUpdateCancel; + label1.Text = "Checking for updates . . ."; + changelogTreeView.Visible = false; + changelogTreeView.Location = new(12, 41); + changelogTreeView.Size = new(380, 208); + + GithubPackageResolver resolver = new("pointfeev", "CreamInstaller", "CreamInstaller.zip"); + ZipPackageExtractor extractor = new(); + updateManager = new(AssemblyMetadata.FromAssembly(Program.EntryAssembly, Program.CurrentProcessFilePath), resolver, extractor); + + if (latestVersion is null) { - if (cancellationTokenSource is not null) - { - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; - } - Hide(); - new SelectForm(this).ShowDialog(); - Close(); - } - - private static readonly HttpClient httpClient = new(); - private static UpdateManager updateManager = null; - private static Version latestVersion = null; - private static IReadOnlyList versions; - - private async void OnLoad() - { - Size = new(420, 85); - progressBar1.Visible = false; - ignoreButton.Visible = true; - updateButton.Text = "Update"; - updateButton.Click -= OnUpdateCancel; - label1.Text = "Checking for updates . . ."; - changelogTreeView.Visible = false; - changelogTreeView.Location = new(12, 41); - changelogTreeView.Size = new(380, 208); - - GithubPackageResolver resolver = new("pointfeev", "CreamInstaller", "CreamInstaller.zip"); - ZipPackageExtractor extractor = new(); - updateManager = new(AssemblyMetadata.FromAssembly(Program.EntryAssembly, Program.CurrentProcessFilePath), resolver, extractor); - - if (latestVersion is null) - { - CheckForUpdatesResult checkForUpdatesResult = null; - cancellationTokenSource = new(); - try - { - checkForUpdatesResult = await updateManager.CheckForUpdatesAsync(cancellationTokenSource.Token); - cancellationTokenSource.Dispose(); - cancellationTokenSource = null; - if (checkForUpdatesResult.CanUpdate) - { - latestVersion = checkForUpdatesResult.LastVersion; - versions = checkForUpdatesResult.Versions; - } - } - catch { } - } - - if (latestVersion is null) - { - updateManager.Dispose(); - StartProgram(); - } - else - { - Size = new(420, 300); - label1.Text = $"An update is available: v{latestVersion}"; - ignoreButton.Enabled = true; - updateButton.Enabled = true; - updateButton.Click += new(OnUpdate); - changelogTreeView.Visible = true; - Version currentVersion = new(Application.ProductVersion); - foreach (Version version in versions) - if (version > currentVersion && !changelogTreeView.Nodes.ContainsKey(version.ToString())) - { - TreeNode root = new($"v{version}"); - root.Name = root.Text; - changelogTreeView.Nodes.Add(root); - _ = Task.Run(async () => - { - try - { - string url = $"https://github.com/pointfeev/CreamInstaller/releases/tag/v{version}"; - using HttpRequestMessage request = new(HttpMethod.Get, url); - using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - using Stream stream = await response.Content.ReadAsStreamAsync(); - using StreamReader reader = new(stream, Encoding.UTF8); - HtmlAgilityPack.HtmlDocument document = new(); - document.LoadHtml(reader.ReadToEnd()); - foreach (HtmlNode node in document.DocumentNode.SelectNodes("//div[@data-test-selector='body-content']/ul/li")) - { - changelogTreeView.Invoke((MethodInvoker)delegate - { - TreeNode change = new(); - change.Text = $"{HttpUtility.HtmlDecode(node.InnerText)}"; - root.Nodes.Add(change); - root.Expand(); - }); - } - } - catch - { - changelogTreeView.Nodes.Remove(root); - } - }); - } - } - } - - private void OnLoad(object sender, EventArgs _) - { - retry: - try - { - string FileName = Path.GetFileName(Program.CurrentProcessFilePath); - if (FileName != "CreamInstaller.exe") - if (new DialogForm(this).Show(Program.ApplicationName, SystemIcons.Warning, - "WARNING: CreamInstaller.exe was renamed!" + - "\n\nThis will cause unwanted behavior when updating the program!", - "Ignore", "Abort") == DialogResult.Cancel) - { - Application.Exit(); - return; - } - OnLoad(); - } - catch (Exception e) - { - if (ExceptionHandler.OutputException(e)) goto retry; - Close(); - } - } - - private void OnIgnore(object sender, EventArgs e) => StartProgram(); - - private async void OnUpdate(object sender, EventArgs e) - { - progressBar1.Visible = true; - ignoreButton.Visible = false; - updateButton.Text = "Cancel"; - updateButton.Click -= OnUpdate; - updateButton.Click += new(OnUpdateCancel); - changelogTreeView.Location = new(12, 70); - changelogTreeView.Size = new(380, 179); - - Progress progress = new(); - progress.ProgressChanged += new(delegate (object sender, double _progress) - { - label1.Text = $"Updating . . . {(int)_progress}%"; - progressBar1.Value = (int)_progress; - }); - - label1.Text = "Updating . . . "; + CheckForUpdatesResult checkForUpdatesResult = null; cancellationTokenSource = new(); try { - await updateManager.PrepareUpdateAsync(latestVersion, progress, cancellationTokenSource.Token); + checkForUpdatesResult = await updateManager.CheckForUpdatesAsync(cancellationTokenSource.Token); cancellationTokenSource.Dispose(); cancellationTokenSource = null; + if (checkForUpdatesResult.CanUpdate) + { + latestVersion = checkForUpdatesResult.LastVersion; + versions = checkForUpdatesResult.Versions; + } } catch { } - - if (updateManager is not null && updateManager.IsUpdatePrepared(latestVersion)) - { - updateManager.LaunchUpdater(latestVersion); - Application.Exit(); - return; - } - else OnLoad(); } - private void OnUpdateCancel(object sender, EventArgs e) + if (latestVersion is null) { - cancellationTokenSource.Cancel(); updateManager.Dispose(); - updateManager = null; + StartProgram(); + } + else + { + Size = new(420, 300); + label1.Text = $"An update is available: v{latestVersion}"; + ignoreButton.Enabled = true; + updateButton.Enabled = true; + updateButton.Click += new(OnUpdate); + changelogTreeView.Visible = true; + Version currentVersion = new(Application.ProductVersion); + foreach (Version version in versions) + if (version > currentVersion && !changelogTreeView.Nodes.ContainsKey(version.ToString())) + { + TreeNode root = new($"v{version}"); + root.Name = root.Text; + changelogTreeView.Nodes.Add(root); + _ = Task.Run(async () => + { + try + { + string url = $"https://github.com/pointfeev/CreamInstaller/releases/tag/v{version}"; + using HttpRequestMessage request = new(HttpMethod.Get, url); + using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + using Stream stream = await response.Content.ReadAsStreamAsync(); + using StreamReader reader = new(stream, Encoding.UTF8); + HtmlAgilityPack.HtmlDocument document = new(); + document.LoadHtml(reader.ReadToEnd()); + foreach (HtmlNode node in document.DocumentNode.SelectNodes("//div[@data-test-selector='body-content']/ul/li")) + { + changelogTreeView.Invoke((MethodInvoker)delegate + { + TreeNode change = new(); + change.Text = $"{HttpUtility.HtmlDecode(node.InnerText)}"; + root.Nodes.Add(change); + root.Expand(); + }); + } + } + catch + { + changelogTreeView.Nodes.Remove(root); + } + }); + } } } -} \ No newline at end of file + + private void OnLoad(object sender, EventArgs _) + { + retry: + try + { + string FileName = Path.GetFileName(Program.CurrentProcessFilePath); + if (FileName != "CreamInstaller.exe") + if (new DialogForm(this).Show(Program.ApplicationName, SystemIcons.Warning, + "WARNING: CreamInstaller.exe was renamed!" + + "\n\nThis will cause unwanted behavior when updating the program!", + "Ignore", "Abort") == DialogResult.Cancel) + { + Application.Exit(); + return; + } + OnLoad(); + } + catch (Exception e) + { + if (ExceptionHandler.OutputException(e)) goto retry; + Close(); + } + } + + private void OnIgnore(object sender, EventArgs e) => StartProgram(); + + private async void OnUpdate(object sender, EventArgs e) + { + progressBar1.Visible = true; + ignoreButton.Visible = false; + updateButton.Text = "Cancel"; + updateButton.Click -= OnUpdate; + updateButton.Click += new(OnUpdateCancel); + changelogTreeView.Location = new(12, 70); + changelogTreeView.Size = new(380, 179); + + Progress progress = new(); + progress.ProgressChanged += new(delegate (object sender, double _progress) + { + label1.Text = $"Updating . . . {(int)_progress}%"; + progressBar1.Value = (int)_progress; + }); + + label1.Text = "Updating . . . "; + cancellationTokenSource = new(); + try + { + await updateManager.PrepareUpdateAsync(latestVersion, progress, cancellationTokenSource.Token); + cancellationTokenSource.Dispose(); + cancellationTokenSource = null; + } + catch { } + + if (updateManager is not null && updateManager.IsUpdatePrepared(latestVersion)) + { + updateManager.LaunchUpdater(latestVersion); + Application.Exit(); + return; + } + else OnLoad(); + } + + private void OnUpdateCancel(object sender, EventArgs e) + { + cancellationTokenSource.Cancel(); + updateManager.Dispose(); + updateManager = null; + } +} diff --git a/CreamInstaller/Forms/SelectForm.cs b/CreamInstaller/Forms/SelectForm.cs index bfa5d4b..9bc16f0 100644 --- a/CreamInstaller/Forms/SelectForm.cs +++ b/CreamInstaller/Forms/SelectForm.cs @@ -14,538 +14,532 @@ using Gameloop.Vdf.Linq; using Microsoft.Win32; -namespace CreamInstaller +namespace CreamInstaller; + +internal partial class SelectForm : CustomForm { - internal partial class SelectForm : CustomForm + internal SelectForm(IWin32Window owner) : base(owner) { - internal SelectForm(IWin32Window owner) : base(owner) - { - InitializeComponent(); - Text = Program.ApplicationName; - Program.SelectForm = this; - } + InitializeComponent(); + Text = Program.ApplicationName; + Program.SelectForm = this; + } - private static async Task> GameLibraryDirectories() => await Task.Run(() => + private static async Task> GameLibraryDirectories() => await Task.Run(() => + { + List gameDirectories = new(); + if (Program.Canceled) return gameDirectories; + string steamInstallPath = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Steam", "InstallPath", null) as string; + steamInstallPath ??= Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Valve\\Steam", "InstallPath", null) as string; + if (steamInstallPath != null && Directory.Exists(steamInstallPath)) { - List gameDirectories = new(); - if (Program.Canceled) return gameDirectories; - string steamInstallPath = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Steam", "InstallPath", null) as string; - steamInstallPath ??= Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Valve\\Steam", "InstallPath", null) as string; - if (steamInstallPath != null && Directory.Exists(steamInstallPath)) + string libraryFolder = steamInstallPath + @"\steamapps"; + if (Directory.Exists(libraryFolder)) { - string libraryFolder = steamInstallPath + @"\steamapps"; - if (Directory.Exists(libraryFolder)) + gameDirectories.Add(libraryFolder); + string libraryFolders = libraryFolder + @"\libraryfolders.vdf"; + if (File.Exists(libraryFolders) && ValveDataFile.TryDeserialize(File.ReadAllText(libraryFolders, Encoding.UTF8), out VProperty result)) { - gameDirectories.Add(libraryFolder); - string libraryFolders = libraryFolder + @"\libraryfolders.vdf"; - if (File.Exists(libraryFolders) && ValveDataFile.TryDeserialize(File.ReadAllText(libraryFolders, Encoding.UTF8), out VProperty _result)) - { - dynamic result = _result; - foreach (dynamic property in result?.Value) if (int.TryParse(property.Key, out int _)) - { - string path = property.Value?.path?.ToString(); - if (string.IsNullOrWhiteSpace(path)) continue; - path += @"\steamapps"; - if (Directory.Exists(path) && !gameDirectories.Contains(path)) gameDirectories.Add(path); - } - } + foreach (VProperty property in result.Value) + if (int.TryParse(property.Key, out int _)) + { + string path = property.Value.GetChild("path")?.ToString(); + if (string.IsNullOrWhiteSpace(path)) continue; + path += @"\steamapps"; + if (Directory.Exists(path) && !gameDirectories.Contains(path)) gameDirectories.Add(path); + } } } - return gameDirectories; - }); - - private static async Task> GetDllDirectoriesFromGameDirectory(string gameDirectory) => await Task.Run(async () => - { - List dllDirectories = new(); - if (Program.Canceled || !Directory.Exists(gameDirectory)) return null; - string api = gameDirectory + @"\steam_api.dll"; - string api64 = gameDirectory + @"\steam_api64.dll"; - if (File.Exists(api) || File.Exists(api64)) dllDirectories.Add(gameDirectory); - string[] directories = Directory.GetDirectories(gameDirectory); - foreach (string _directory in directories) - { - if (Program.Canceled) return null; - try - { - List moreDllDirectories = await GetDllDirectoriesFromGameDirectory(_directory); - if (moreDllDirectories is not null) dllDirectories.AddRange(moreDllDirectories); - } - catch { } - } - if (!dllDirectories.Any()) return null; - return dllDirectories; - }); - - private static async Task>> GetGamesFromLibraryDirectory(string libraryDirectory) => await Task.Run(() => - { - List> games = new(); - if (Program.Canceled || !Directory.Exists(libraryDirectory)) return null; - string[] files = Directory.GetFiles(libraryDirectory); - foreach (string file in files) - { - if (Program.Canceled) return null; - if (Path.GetExtension(file) == ".acf" && ValveDataFile.TryDeserialize(File.ReadAllText(file, Encoding.UTF8), out VProperty _result)) - { - dynamic result = _result; - string _appid = result.Value?.appid?.ToString(); - string installdir = result.Value?.installdir?.ToString(); - string name = result.Value?.name?.ToString(); - string _buildid = result.Value?.buildid?.ToString(); - if (string.IsNullOrWhiteSpace(_appid) - || string.IsNullOrWhiteSpace(installdir) - || string.IsNullOrWhiteSpace(name) - || string.IsNullOrWhiteSpace(_buildid)) - continue; - string branch = result.Value?.UserConfig?.betakey?.ToString(); - if (string.IsNullOrWhiteSpace(branch)) branch = "public"; - string gameDirectory = libraryDirectory + @"\common\" + installdir; - if (!int.TryParse(_appid, out int appid)) continue; - if (!int.TryParse(_buildid, out int buildid)) continue; - games.Add(new(appid, name, branch, buildid, gameDirectory)); - } - } - if (!games.Any()) return null; - return games; - }); - - internal List TreeNodes => GatherTreeNodes(selectionTreeView.Nodes); - private List GatherTreeNodes(TreeNodeCollection nodeCollection) - { - List treeNodes = new(); - foreach (TreeNode rootNode in nodeCollection) - { - treeNodes.Add(rootNode); - treeNodes.AddRange(GatherTreeNodes(rootNode.Nodes)); - } - return treeNodes; } + return gameDirectories; + }); - internal List RunningTasks = new(); - - private async Task GetCreamApiApplicablePrograms(IProgress progress) + private static async Task> GetDllDirectoriesFromGameDirectory(string gameDirectory) => await Task.Run(async () => + { + List dllDirectories = new(); + if (Program.Canceled || !Directory.Exists(gameDirectory)) return null; + string api = gameDirectory + @"\steam_api.dll"; + string api64 = gameDirectory + @"\steam_api64.dll"; + if (File.Exists(api) || File.Exists(api64)) dllDirectories.Add(gameDirectory); + string[] directories = Directory.GetDirectories(gameDirectory); + foreach (string _directory in directories) { - if (Program.Canceled) return; - List> applicablePrograms = new(); - string launcherRootDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Programs\\Paradox Interactive"; - if (Directory.Exists(launcherRootDirectory)) - applicablePrograms.Add(new(0, "Paradox Launcher", "", 0, launcherRootDirectory)); - List gameLibraryDirectories = await GameLibraryDirectories(); - foreach (string libraryDirectory in gameLibraryDirectories) - { - List> games = await GetGamesFromLibraryDirectory(libraryDirectory); - if (games is not null) - foreach (Tuple game in games) - applicablePrograms.Add(game); - } - - int cur = 0; - RunningTasks.Clear(); - foreach (Tuple program in applicablePrograms) - { - int appId = program.Item1; - string name = program.Item2; - string branch = program.Item3; - int buildId = program.Item4; - string directory = program.Item5; - ProgramSelection selection = ProgramSelection.FromAppId(appId); - if (selection is not null) selection.Validate(); - if (Program.Canceled) return; - if (Program.BlockProtectedGames && Program.IsGameBlocked(name, directory)) continue; - RunningTasks.Add(Task.Run(async () => - { - if (Program.Canceled) return; - List dllDirectories = await GetDllDirectoriesFromGameDirectory(directory); - if (dllDirectories is null) return; - VProperty appInfo = null; - if (appId > 0) appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId); - if (appId > 0 && appInfo is null) return; - if (Program.Canceled) return; - ConcurrentDictionary dlc = new(); - List dlcTasks = new(); - List dlcIds = await SteamCMD.ParseDlcAppIds(appInfo); - if (dlcIds.Count > 0) - { - foreach (int id in dlcIds) - { - if (Program.Canceled) return; - Task task = Task.Run(async () => - { - if (Program.Canceled) return; - string dlcName = null; - VProperty dlcAppInfo = await SteamCMD.GetAppInfo(id); - if (dlcAppInfo is not null) dlcName = dlcAppInfo.Value?.TryGet("common")?.TryGet("name")?.ToString(); - if (Program.Canceled) return; - if (string.IsNullOrWhiteSpace(dlcName)) return; //dlcName = "Unknown DLC"; - dlc[id] = /*$"[{id}] " +*/ dlcName; - progress.Report(++cur); - }); - dlcTasks.Add(task); - RunningTasks.Add(task); - progress.Report(-RunningTasks.Count); - Thread.Sleep(10); // to reduce control & window freezing - } - } - else if (appId > 0) return; - if (Program.Canceled) return; - if (string.IsNullOrWhiteSpace(name)) return; - - selection ??= new(); - selection.Usable = true; - selection.SteamAppId = appId; - selection.Name = name; - selection.RootDirectory = directory; - selection.SteamApiDllDirectories = dllDirectories; - selection.AppInfo = appInfo; - if (selection.Icon is null) - { - if (appId == 0) selection.Icon = Program.GetFileIconImage(directory + @"\launcher\bootstrapper-v2.exe"); - else - { - selection.IconStaticID = appInfo?.Value?.TryGet("common")?.TryGet("icon")?.ToString(); - selection.ClientIconStaticID = appInfo?.Value?.TryGet("common")?.TryGet("clienticon")?.ToString(); - } - } - if (allCheckBox.Checked) selection.Enabled = true; - - foreach (Task task in dlcTasks) - { - if (Program.Canceled) return; - await task; - } - if (Program.Canceled) return; - selectionTreeView.Invoke((MethodInvoker)delegate - { - if (Program.Canceled) return; - TreeNode programNode = TreeNodes.Find(s => s.Name == "" + appId) ?? new(); - programNode.Name = "" + appId; - programNode.Text = /*(appId > 0 ? $"[{appId}] " : "") +*/ name; - programNode.Checked = selection.Enabled; - programNode.Remove(); - selectionTreeView.Nodes.Add(programNode); - if (appId == 0) // paradox launcher - { - // maybe add game and/or dlc choice here? - } - else - { - foreach (KeyValuePair dlcApp in dlc) - { - if (Program.Canceled || programNode is null) return; - selection.AllSteamDlc[dlcApp.Key] = dlcApp.Value; - if (allCheckBox.Checked) selection.SelectedSteamDlc[dlcApp.Key] = dlcApp.Value; - TreeNode dlcNode = TreeNodes.Find(s => s.Name == "" + dlcApp.Key) ?? new(); - dlcNode.Name = "" + dlcApp.Key; - dlcNode.Text = dlcApp.Value; - dlcNode.Checked = selection.SelectedSteamDlc.Contains(dlcApp); - dlcNode.Remove(); - programNode.Nodes.Add(dlcNode); - } - } - }); - - progress.Report(++cur); - })); - progress.Report(-RunningTasks.Count); - } - foreach (Task task in RunningTasks.ToList()) - { - if (Program.Canceled) return; - await task; - } - progress.Report(RunningTasks.Count); - } - - private async void OnLoad() - { - retry: + if (Program.Canceled) return null; try { - Program.Canceled = false; - blockedGamesCheckBox.Enabled = false; - blockProtectedHelpButton.Enabled = false; - cancelButton.Enabled = true; - scanButton.Enabled = false; - noneFoundLabel.Visible = false; - allCheckBox.Enabled = false; - installButton.Enabled = false; - uninstallButton.Enabled = installButton.Enabled; - selectionTreeView.Enabled = false; - progressLabel.Visible = true; - progressBar1.Visible = true; - progressBar1.Value = 0; - groupBox1.Size = new(groupBox1.Size.Width, groupBox1.Size.Height - 44); - - bool setup = true; - int maxProgress = 0; - int curProgress = 0; - Progress progress = new(); - IProgress iProgress = progress; - progress.ProgressChanged += (sender, _progress) => - { - if (Program.Canceled) return; - if (_progress < 0) maxProgress = -_progress; - else curProgress = _progress; - int p = Math.Max(Math.Min((int)((float)(curProgress / (float)maxProgress) * 100), 100), 0); - progressLabel.Text = setup ? $"Setting up SteamCMD . . . {p}% ({curProgress}/{maxProgress})" - : $"Gathering and caching your applicable games and their DLCs . . . {p}% ({curProgress}/{maxProgress})"; - progressBar1.Value = p; - }; - - iProgress.Report(-1660); // not exact, number varies - int cur = 0; - iProgress.Report(cur); - progressLabel.Text = "Setting up SteamCMD . . . "; - if (!Directory.Exists(SteamCMD.DirectoryPath)) Directory.CreateDirectory(SteamCMD.DirectoryPath); - - FileSystemWatcher watcher = new(SteamCMD.DirectoryPath); - watcher.Changed += (sender, e) => iProgress.Report(++cur); - watcher.Filter = "*"; - watcher.IncludeSubdirectories = true; - watcher.EnableRaisingEvents = true; - await SteamCMD.Setup(); - watcher.Dispose(); - - setup = false; - progressLabel.Text = "Gathering and caching your applicable games and their DLCs . . . "; - ProgramSelection.ValidateAll(); - TreeNodes.ForEach(node => - { - if (!int.TryParse(node.Name, out int appId) || node.Parent is null && ProgramSelection.FromAppId(appId) is null) node.Remove(); - }); - await GetCreamApiApplicablePrograms(iProgress); - - progressBar1.Value = 100; - groupBox1.Size = new(groupBox1.Size.Width, groupBox1.Size.Height + 44); - progressLabel.Visible = false; - progressBar1.Visible = false; - selectionTreeView.Enabled = ProgramSelection.All.Any(); - allCheckBox.Enabled = selectionTreeView.Enabled; - noneFoundLabel.Visible = !selectionTreeView.Enabled; - installButton.Enabled = ProgramSelection.AllUsableEnabled.Any(); - uninstallButton.Enabled = installButton.Enabled; - cancelButton.Enabled = false; - scanButton.Enabled = true; - blockedGamesCheckBox.Enabled = true; - blockProtectedHelpButton.Enabled = true; + List moreDllDirectories = await GetDllDirectoriesFromGameDirectory(_directory); + if (moreDllDirectories is not null) dllDirectories.AddRange(moreDllDirectories); } - catch (Exception e) + catch { } + } + return !dllDirectories.Any() ? null : dllDirectories; + }); + + private static async Task>> GetGamesFromLibraryDirectory(string libraryDirectory) => await Task.Run(() => + { + List> games = new(); + if (Program.Canceled || !Directory.Exists(libraryDirectory)) return null; + string[] files = Directory.GetFiles(libraryDirectory); + foreach (string file in files) + { + if (Program.Canceled) return null; + if (Path.GetExtension(file) == ".acf" && ValveDataFile.TryDeserialize(File.ReadAllText(file, Encoding.UTF8), out VProperty result)) { - if (ExceptionHandler.OutputException(e)) goto retry; - Close(); + string appId = result.Value.GetChild("appid")?.ToString(); + string installdir = result.Value.GetChild("installdir")?.ToString(); + string name = result.Value.GetChild("name")?.ToString(); + string buildId = result.Value.GetChild("buildid")?.ToString(); + if (string.IsNullOrWhiteSpace(appId) + || string.IsNullOrWhiteSpace(installdir) + || string.IsNullOrWhiteSpace(name) + || string.IsNullOrWhiteSpace(buildId)) + continue; + string branch = result.Value.GetChild("UserConfig")?.GetChild("betakey")?.ToString(); + if (string.IsNullOrWhiteSpace(branch)) branch = "public"; + string gameDirectory = libraryDirectory + @"\common\" + installdir; + if (!int.TryParse(appId, out int appIdInt)) continue; + if (!int.TryParse(buildId, out int buildIdInt)) continue; + games.Add(new(appIdInt, name, branch, buildIdInt, gameDirectory)); } } + return !games.Any() ? null : games; + }); - private void OnTreeViewNodeCheckedChanged(object sender, TreeViewEventArgs e) + internal List TreeNodes => GatherTreeNodes(selectionTreeView.Nodes); + private List GatherTreeNodes(TreeNodeCollection nodeCollection) + { + List treeNodes = new(); + foreach (TreeNode rootNode in nodeCollection) { - if (e.Action == TreeViewAction.Unknown) return; - TreeNode node = e.Node; - if (node is not null) + treeNodes.Add(rootNode); + treeNodes.AddRange(GatherTreeNodes(rootNode.Nodes)); + } + return treeNodes; + } + + internal List RunningTasks = new(); + + private async Task GetCreamApiApplicablePrograms(IProgress progress) + { + if (Program.Canceled) return; + List> applicablePrograms = new(); + string launcherRootDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Programs\\Paradox Interactive"; + if (Directory.Exists(launcherRootDirectory)) + applicablePrograms.Add(new(0, "Paradox Launcher", "", 0, launcherRootDirectory)); + List gameLibraryDirectories = await GameLibraryDirectories(); + foreach (string libraryDirectory in gameLibraryDirectories) + { + List> games = await GetGamesFromLibraryDirectory(libraryDirectory); + if (games is not null) + foreach (Tuple game in games) + applicablePrograms.Add(game); + } + + int CompleteTasks = 0; + RunningTasks.Clear(); + foreach (Tuple program in applicablePrograms) + { + int appId = program.Item1; + string name = program.Item2; + string branch = program.Item3; + int buildId = program.Item4; + string directory = program.Item5; + ProgramSelection selection = ProgramSelection.FromAppId(appId); + if (Program.Canceled) return; + if (Program.BlockProtectedGames && Program.IsGameBlocked(name, directory)) continue; + RunningTasks.Add(Task.Run(async () => { - ProgramSelection selection = ProgramSelection.FromAppId(int.Parse(node.Name)); - if (selection is null) + if (Program.Canceled) return; + List dllDirectories = await GetDllDirectoriesFromGameDirectory(directory); + if (dllDirectories is null) return; + VProperty appInfo = null; + if (appId > 0) appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId); + if (appId > 0 && appInfo is null) return; + if (Program.Canceled) return; + ConcurrentDictionary dlc = new(); + List dlcTasks = new(); + List dlcIds = await SteamCMD.ParseDlcAppIds(appInfo); + if (dlcIds.Count > 0) { - TreeNode parent = node.Parent; - if (parent is not null) + foreach (int id in dlcIds) { - ProgramSelection.FromAppId(int.Parse(parent.Name)).ToggleDlc(int.Parse(node.Name), node.Checked); - parent.Checked = parent.Nodes.Cast().ToList().Any(treeNode => treeNode.Checked); + if (Program.Canceled) return; + Task task = Task.Run(async () => + { + if (Program.Canceled) return; + string dlcName = null; + VProperty dlcAppInfo = await SteamCMD.GetAppInfo(id); + if (dlcAppInfo is not null) dlcName = dlcAppInfo.Value?.GetChild("common")?.GetChild("name")?.ToString(); + if (Program.Canceled) return; + if (string.IsNullOrWhiteSpace(dlcName)) return; //dlcName = "Unknown DLC"; + dlc[id] = /*$"[{id}] " +*/ dlcName; + progress.Report(++CompleteTasks); + }); + dlcTasks.Add(task); + RunningTasks.Add(task); + progress.Report(-RunningTasks.Count); + Thread.Sleep(10); // to reduce control & window freezing + } + } + else if (appId > 0) return; + if (Program.Canceled) return; + if (string.IsNullOrWhiteSpace(name)) return; + + selection ??= new(); + selection.Usable = true; + selection.SteamAppId = appId; + selection.Name = name; + selection.RootDirectory = directory; + selection.SteamApiDllDirectories = dllDirectories; + selection.AppInfo = appInfo; + if (selection.Icon is null) + { + if (appId == 0) selection.Icon = Program.GetFileIconImage(directory + @"\launcher\bootstrapper-v2.exe"); + else + { + selection.IconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString(); + selection.ClientIconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString(); + } + } + if (allCheckBox.Checked) selection.Enabled = true; + + foreach (Task task in dlcTasks) + { + if (Program.Canceled) return; + await task; + } + if (Program.Canceled) return; + selectionTreeView.Invoke((MethodInvoker)delegate + { + if (Program.Canceled) return; + TreeNode programNode = TreeNodes.Find(s => s.Name == "" + appId) ?? new(); + programNode.Name = "" + appId; + programNode.Text = /*(appId > 0 ? $"[{appId}] " : "") +*/ name; + programNode.Checked = selection.Enabled; + programNode.Remove(); + selectionTreeView.Nodes.Add(programNode); + if (appId == 0) // paradox launcher + { + // maybe add game and/or dlc choice here? + } + else + { + foreach (KeyValuePair dlcApp in dlc) + { + if (Program.Canceled || programNode is null) return; + selection.AllSteamDlc[dlcApp.Key] = dlcApp.Value; + if (allCheckBox.Checked) selection.SelectedSteamDlc[dlcApp.Key] = dlcApp.Value; + TreeNode dlcNode = TreeNodes.Find(s => s.Name == "" + dlcApp.Key) ?? new(); + dlcNode.Name = "" + dlcApp.Key; + dlcNode.Text = dlcApp.Value; + dlcNode.Checked = selection.SelectedSteamDlc.Contains(dlcApp); + dlcNode.Remove(); + programNode.Nodes.Add(dlcNode); + } + } + }); + progress.Report(++CompleteTasks); + })); + progress.Report(-RunningTasks.Count); + } + foreach (Task task in RunningTasks.ToList()) + { + if (Program.Canceled) return; + await task; + } + progress.Report(RunningTasks.Count); + } + + private async void OnLoad() + { + retry: + try + { + Program.Canceled = false; + blockedGamesCheckBox.Enabled = false; + blockProtectedHelpButton.Enabled = false; + cancelButton.Enabled = true; + scanButton.Enabled = false; + noneFoundLabel.Visible = false; + allCheckBox.Enabled = false; + installButton.Enabled = false; + uninstallButton.Enabled = installButton.Enabled; + selectionTreeView.Enabled = false; + progressLabel.Visible = true; + progressBar1.Visible = true; + progressBar1.Value = 0; + groupBox1.Size = new(groupBox1.Size.Width, groupBox1.Size.Height - 44); + + bool setup = true; + int maxProgress = 0; + int curProgress = 0; + Progress progress = new(); + IProgress iProgress = progress; + progress.ProgressChanged += (sender, _progress) => + { + if (Program.Canceled) return; + if (_progress < 0) maxProgress = -_progress; + else curProgress = _progress; + int p = Math.Max(Math.Min((int)((float)(curProgress / (float)maxProgress) * 100), 100), 0); + progressLabel.Text = setup ? $"Setting up SteamCMD . . . {p}% ({curProgress}/{maxProgress})" + : $"Gathering and caching your applicable games and their DLCs . . . {p}% ({curProgress}/{maxProgress})"; + progressBar1.Value = p; + }; + + iProgress.Report(-1660); // not exact, number varies + int cur = 0; + iProgress.Report(cur); + progressLabel.Text = "Setting up SteamCMD . . . "; + if (!Directory.Exists(SteamCMD.DirectoryPath)) Directory.CreateDirectory(SteamCMD.DirectoryPath); + + FileSystemWatcher watcher = new(SteamCMD.DirectoryPath); + watcher.Changed += (sender, e) => iProgress.Report(++cur); + watcher.Filter = "*"; + watcher.IncludeSubdirectories = true; + watcher.EnableRaisingEvents = true; + await SteamCMD.Setup(); + watcher.Dispose(); + + setup = false; + progressLabel.Text = "Gathering and caching your applicable games and their DLCs . . . "; + ProgramSelection.ValidateAll(); + TreeNodes.ForEach(node => + { + if (!int.TryParse(node.Name, out int appId) || node.Parent is null && ProgramSelection.FromAppId(appId) is null) node.Remove(); + }); + await GetCreamApiApplicablePrograms(iProgress); + + progressBar1.Value = 100; + groupBox1.Size = new(groupBox1.Size.Width, groupBox1.Size.Height + 44); + progressLabel.Visible = false; + progressBar1.Visible = false; + selectionTreeView.Enabled = ProgramSelection.All.Any(); + allCheckBox.Enabled = selectionTreeView.Enabled; + noneFoundLabel.Visible = !selectionTreeView.Enabled; + installButton.Enabled = ProgramSelection.AllUsableEnabled.Any(); + uninstallButton.Enabled = installButton.Enabled; + cancelButton.Enabled = false; + scanButton.Enabled = true; + blockedGamesCheckBox.Enabled = true; + blockProtectedHelpButton.Enabled = true; + } + catch (Exception e) + { + if (ExceptionHandler.OutputException(e)) goto retry; + Close(); + } + } + + private void OnTreeViewNodeCheckedChanged(object sender, TreeViewEventArgs e) + { + if (e.Action == TreeViewAction.Unknown) return; + TreeNode node = e.Node; + if (node is not null) + { + ProgramSelection selection = ProgramSelection.FromAppId(int.Parse(node.Name)); + if (selection is null) + { + TreeNode parent = node.Parent; + if (parent is not null) + { + ProgramSelection.FromAppId(int.Parse(parent.Name)).ToggleDlc(int.Parse(node.Name), node.Checked); + parent.Checked = parent.Nodes.Cast().ToList().Any(treeNode => treeNode.Checked); + } + } + else + { + if (selection.AllSteamDlc.Any()) + { + selection.ToggleAllDlc(node.Checked); + node.Nodes.Cast().ToList().ForEach(treeNode => treeNode.Checked = node.Checked); + } + else selection.Enabled = node.Checked; + allCheckBox.CheckedChanged -= OnAllCheckBoxChanged; + allCheckBox.Checked = TreeNodes.TrueForAll(treeNode => treeNode.Checked); + allCheckBox.CheckedChanged += OnAllCheckBoxChanged; + } + } + installButton.Enabled = ProgramSelection.AllUsableEnabled.Any(); + uninstallButton.Enabled = installButton.Enabled; + } + + private class TreeNodeSorter : IComparer + { + public int Compare(object a, object b) + { + if (!int.TryParse((a as TreeNode).Name, out int A)) return 1; + if (!int.TryParse((b as TreeNode).Name, out int B)) return 0; + return A > B ? 1 : 0; + } + } + + private void OnLoad(object sender, EventArgs _) + { + selectionTreeView.TreeViewNodeSorter = new TreeNodeSorter(); + selectionTreeView.AfterCheck += OnTreeViewNodeCheckedChanged; + Dictionary images = new(); + Task.Run(async () => + { + images["File Explorer"] = Program.GetFileExplorerImage(); + images["SteamDB"] = await Program.GetImageFromUrl("https://steamdb.info/favicon.ico"); + images["Steam Store"] = await Program.GetImageFromUrl("https://store.steampowered.com/favicon.ico"); + images["Steam Community"] = await Program.GetImageFromUrl("https://steamcommunity.com/favicon.ico"); + }); + Image Image(string identifier) => images.GetValueOrDefault(identifier, null); + selectionTreeView.NodeMouseClick += (sender, e) => + { + TreeNode node = e.Node; + TreeNode parentNode = node.Parent; + if (!int.TryParse(node.Name, out int appId)) return; + ProgramSelection selection = ProgramSelection.FromAppId(appId); + if (e.Button == MouseButtons.Right && node.Bounds.Contains(e.Location)) + { + selectionTreeView.SelectedNode = node; + nodeContextMenu.Items.Clear(); + if (selection is not null) + { + nodeContextMenu.Items.Add(new ToolStripMenuItem(selection.Name, selection.Icon)); + nodeContextMenu.Items.Add(new ToolStripSeparator()); + nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Root Directory", Image("File Explorer"), + new EventHandler((sender, e) => Program.OpenDirectoryInFileExplorer(selection.RootDirectory)))); + for (int i = 0; i < selection.SteamApiDllDirectories.Count; i++) + { + string directory = selection.SteamApiDllDirectories[i]; + nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open Steamworks Directory ({i + 1})", Image("File Explorer"), + new EventHandler((sender, e) => Program.OpenDirectoryInFileExplorer(directory)))); } } else { - if (selection.AllSteamDlc.Any()) - { - selection.ToggleAllDlc(node.Checked); - node.Nodes.Cast().ToList().ForEach(treeNode => treeNode.Checked = node.Checked); - } - else selection.Enabled = node.Checked; - allCheckBox.CheckedChanged -= OnAllCheckBoxChanged; - allCheckBox.Checked = TreeNodes.TrueForAll(treeNode => treeNode.Checked); - allCheckBox.CheckedChanged += OnAllCheckBoxChanged; + nodeContextMenu.Items.Add(new ToolStripMenuItem(node.Text)); + nodeContextMenu.Items.Add(new ToolStripSeparator()); } - } - installButton.Enabled = ProgramSelection.AllUsableEnabled.Any(); - uninstallButton.Enabled = installButton.Enabled; - } - - private class TreeNodeSorter : IComparer - { - public int Compare(object a, object b) - { - if (!int.TryParse((a as TreeNode).Name, out int A)) return 1; - if (!int.TryParse((b as TreeNode).Name, out int B)) return 0; - return A > B ? 1 : 0; - } - } - - private void OnLoad(object sender, EventArgs _) - { - selectionTreeView.TreeViewNodeSorter = new TreeNodeSorter(); - selectionTreeView.AfterCheck += OnTreeViewNodeCheckedChanged; - Dictionary images = new(); - Task.Run(async () => - { - images["File Explorer"] = Program.GetFileExplorerImage(); - images["SteamDB"] = await Program.GetImageFromUrl("https://steamdb.info/favicon.ico"); - images["Steam Store"] = await Program.GetImageFromUrl("https://store.steampowered.com/favicon.ico"); - images["Steam Community"] = await Program.GetImageFromUrl("https://steamcommunity.com/favicon.ico"); - }); - Image Image(string identifier) => images.GetValueOrDefault(identifier, null); - selectionTreeView.NodeMouseClick += (sender, e) => - { - TreeNode node = e.Node; - TreeNode parentNode = node.Parent; - if (!int.TryParse(node.Name, out int appId)) return; - ProgramSelection selection = ProgramSelection.FromAppId(appId); - if (e.Button == MouseButtons.Right && node.Bounds.Contains(e.Location)) + if (appId != 0) { - selectionTreeView.SelectedNode = node; - nodeContextMenu.Items.Clear(); - if (selection is not null) - { - nodeContextMenu.Items.Add(new ToolStripMenuItem(selection.Name, selection.Icon)); - nodeContextMenu.Items.Add(new ToolStripSeparator()); - nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Root Directory", Image("File Explorer"), - new EventHandler((sender, e) => Program.OpenDirectoryInFileExplorer(selection.RootDirectory)))); - for (int i = 0; i < selection.SteamApiDllDirectories.Count; i++) - { - string directory = selection.SteamApiDllDirectories[i]; - nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open Steamworks Directory ({i + 1})", Image("File Explorer"), - new EventHandler((sender, e) => Program.OpenDirectoryInFileExplorer(directory)))); - } - } - else - { - nodeContextMenu.Items.Add(new ToolStripMenuItem(node.Text)); - nodeContextMenu.Items.Add(new ToolStripSeparator()); - } - if (appId != 0) - { - nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"), - new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://steamdb.info/app/" + appId)))); - nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Store", Image("Steam Store"), - new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://store.steampowered.com/app/" + appId)))); - if (selection is not null) nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Community", selection.ClientIcon ?? Image("Steam Community"), - new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://steamcommunity.com/app/" + appId)))); - } - nodeContextMenu.Show(selectionTreeView, e.Location); + nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"), + new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://steamdb.info/app/" + appId)))); + nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Store", Image("Steam Store"), + new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://store.steampowered.com/app/" + appId)))); + if (selection is not null) nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Community", selection.ClientIcon ?? Image("Steam Community"), + new EventHandler((sender, e) => Program.OpenUrlInInternetBrowser("https://steamcommunity.com/app/" + appId)))); } - }; - OnLoad(); - } + nodeContextMenu.Show(selectionTreeView, e.Location); + } + }; + OnLoad(); + } - private static void PopulateParadoxLauncherDlc(ProgramSelection paradoxLauncher = null) + private static void PopulateParadoxLauncherDlc(ProgramSelection paradoxLauncher = null) + { + paradoxLauncher ??= ProgramSelection.FromAppId(0); + if (paradoxLauncher is not null) { - paradoxLauncher ??= ProgramSelection.FromAppId(0); - if (paradoxLauncher is not null) + paradoxLauncher.ExtraSteamAppIdDlc.Clear(); + foreach (ProgramSelection selection in ProgramSelection.AllUsableEnabled) { - paradoxLauncher.ExtraSteamAppIdDlc.Clear(); - foreach (ProgramSelection selection in ProgramSelection.AllUsableEnabled) + if (selection.Name == paradoxLauncher.Name) continue; + if (selection.AppInfo.Value?.GetChild("extended")?.GetChild("publisher")?.ToString() != "Paradox Interactive") continue; + paradoxLauncher.ExtraSteamAppIdDlc.Add(new(selection.SteamAppId, selection.Name, selection.SelectedSteamDlc)); + } + if (!paradoxLauncher.ExtraSteamAppIdDlc.Any()) + foreach (ProgramSelection selection in ProgramSelection.AllUsable) { if (selection.Name == paradoxLauncher.Name) continue; - if (selection.AppInfo.Value?.TryGet("extended")?.TryGet("publisher")?.ToString() != "Paradox Interactive") continue; - paradoxLauncher.ExtraSteamAppIdDlc.Add(new(selection.SteamAppId, selection.Name, selection.SelectedSteamDlc)); + if (selection.AppInfo.Value?.GetChild("extended")?.GetChild("publisher")?.ToString() != "Paradox Interactive") continue; + paradoxLauncher.ExtraSteamAppIdDlc.Add(new(selection.SteamAppId, selection.Name, selection.AllSteamDlc)); } - if (!paradoxLauncher.ExtraSteamAppIdDlc.Any()) - foreach (ProgramSelection selection in ProgramSelection.AllUsable) - { - if (selection.Name == paradoxLauncher.Name) continue; - if (selection.AppInfo.Value?.TryGet("extended")?.TryGet("publisher")?.ToString() != "Paradox Interactive") continue; - paradoxLauncher.ExtraSteamAppIdDlc.Add(new(selection.SteamAppId, selection.Name, selection.AllSteamDlc)); - } - } - } - - private static bool ParadoxLauncherDlcDialog(Form form) - { - ProgramSelection paradoxLauncher = ProgramSelection.FromAppId(0); - if (paradoxLauncher is not null && paradoxLauncher.Enabled) - { - PopulateParadoxLauncherDlc(paradoxLauncher); - if (!paradoxLauncher.ExtraSteamAppIdDlc.Any()) - { - return new DialogForm(form).Show(Program.ApplicationName, SystemIcons.Warning, - $"WARNING: There are no installed games with DLC that can be added to the Paradox Launcher!" + - "\n\nInstalling CreamAPI for the Paradox Launcher is pointless, since no DLC will be added to the configuration!", - "Ignore", "Cancel") != DialogResult.OK; - } - } - return false; - } - - private void OnAccept(bool uninstall = false) - { - if (ProgramSelection.All.Any()) - { - foreach (ProgramSelection selection in ProgramSelection.AllUsableEnabled) - if (!Program.IsProgramRunningDialog(this, selection)) return; - if (ParadoxLauncherDlcDialog(this)) return; - Hide(); - InstallForm installForm = new(this, uninstall); - installForm.ShowDialog(); - if (installForm.Reselecting) - { - this.InheritLocation(installForm); - Show(); - OnLoad(); - } - else Close(); - } - } - - private void OnInstall(object sender, EventArgs e) => OnAccept(false); - private void OnUninstall(object sender, EventArgs e) => OnAccept(true); - private void OnScan(object sender, EventArgs e) => OnLoad(); - - private void OnCancel(object sender, EventArgs e) - { - progressLabel.Text = "Cancelling . . . "; - Program.Cleanup(); - } - - private void OnAllCheckBoxChanged(object sender, EventArgs e) - { - bool shouldCheck = false; - TreeNodes.ForEach(node => - { - if (node.Parent is null) - { - if (!node.Checked) shouldCheck = true; - if (node.Checked != shouldCheck) - { - node.Checked = shouldCheck; - OnTreeViewNodeCheckedChanged(null, new(node, TreeViewAction.ByMouse)); - } - } - }); - allCheckBox.Checked = shouldCheck; - } - - private void OnBlockProtectedGamesCheckBoxChanged(object sender, EventArgs e) - { - Program.BlockProtectedGames = blockedGamesCheckBox.Checked; - OnLoad(); - } - - private readonly string helpButtonListPrefix = "\n • "; - private void OnBlockProtectedGamesHelpButtonClicked(object sender, EventArgs e) - { - string blockedGames = ""; - foreach (string name in Program.ProtectedGameNames) - blockedGames += helpButtonListPrefix + name; - string blockedDirectories = ""; - foreach (string path in Program.ProtectedGameDirectories) - blockedDirectories += helpButtonListPrefix + path; - string blockedDirectoryExceptions = ""; - foreach (string name in Program.ProtectedGameDirectoryExceptions) - blockedDirectoryExceptions += helpButtonListPrefix + name; - new DialogForm(this).Show(blockedGamesCheckBox.Text, SystemIcons.Information, - "Blocks the program from caching and displaying games protected by DLL checks," + - "\nanti-cheats, or that are confirmed not to be working with CreamAPI." + - "\n\nBlocked game names:" + blockedGames + - "\n\nBlocked game sub-directories:" + blockedDirectories + - "\n\nBlocked game sub-directory exceptions (not blocked):" + blockedDirectoryExceptions, - "OK"); } } -} \ No newline at end of file + + private static bool ParadoxLauncherDlcDialog(Form form) + { + ProgramSelection paradoxLauncher = ProgramSelection.FromAppId(0); + if (paradoxLauncher is not null && paradoxLauncher.Enabled) + { + PopulateParadoxLauncherDlc(paradoxLauncher); + if (!paradoxLauncher.ExtraSteamAppIdDlc.Any()) + { + return new DialogForm(form).Show(Program.ApplicationName, SystemIcons.Warning, + $"WARNING: There are no installed games with DLC that can be added to the Paradox Launcher!" + + "\n\nInstalling CreamAPI for the Paradox Launcher is pointless, since no DLC will be added to the configuration!", + "Ignore", "Cancel") != DialogResult.OK; + } + } + return false; + } + + private void OnAccept(bool uninstall = false) + { + if (ProgramSelection.All.Any()) + { + foreach (ProgramSelection selection in ProgramSelection.AllUsableEnabled) + if (!Program.IsProgramRunningDialog(this, selection)) return; + if (ParadoxLauncherDlcDialog(this)) return; + Hide(); + InstallForm installForm = new(this, uninstall); + installForm.ShowDialog(); + if (installForm.Reselecting) + { + this.InheritLocation(installForm); + Show(); + OnLoad(); + } + else Close(); + } + } + + private void OnInstall(object sender, EventArgs e) => OnAccept(false); + private void OnUninstall(object sender, EventArgs e) => OnAccept(true); + private void OnScan(object sender, EventArgs e) => OnLoad(); + + private void OnCancel(object sender, EventArgs e) + { + progressLabel.Text = "Cancelling . . . "; + Program.Cleanup(); + } + + private void OnAllCheckBoxChanged(object sender, EventArgs e) + { + bool shouldCheck = false; + TreeNodes.ForEach(node => + { + if (node.Parent is null) + { + if (!node.Checked) shouldCheck = true; + if (node.Checked != shouldCheck) + { + node.Checked = shouldCheck; + OnTreeViewNodeCheckedChanged(null, new(node, TreeViewAction.ByMouse)); + } + } + }); + allCheckBox.Checked = shouldCheck; + } + + private void OnBlockProtectedGamesCheckBoxChanged(object sender, EventArgs e) + { + Program.BlockProtectedGames = blockedGamesCheckBox.Checked; + OnLoad(); + } + + private readonly string helpButtonListPrefix = "\n • "; + private void OnBlockProtectedGamesHelpButtonClicked(object sender, EventArgs e) + { + string blockedGames = ""; + foreach (string name in Program.ProtectedGameNames) + blockedGames += helpButtonListPrefix + name; + string blockedDirectories = ""; + foreach (string path in Program.ProtectedGameDirectories) + blockedDirectories += helpButtonListPrefix + path; + string blockedDirectoryExceptions = ""; + foreach (string name in Program.ProtectedGameDirectoryExceptions) + blockedDirectoryExceptions += helpButtonListPrefix + name; + new DialogForm(this).Show(blockedGamesCheckBox.Text, SystemIcons.Information, + "Blocks the program from caching and displaying games protected by DLL checks," + + "\nanti-cheats, or that are confirmed not to be working with CreamAPI." + + "\n\nBlocked game names:" + blockedGames + + "\n\nBlocked game sub-directories:" + blockedDirectories + + "\n\nBlocked game sub-directory exceptions (not blocked):" + blockedDirectoryExceptions, + "OK"); + } +} diff --git a/CreamInstaller/Program.cs b/CreamInstaller/Program.cs index 6601f54..1fbe404 100644 --- a/CreamInstaller/Program.cs +++ b/CreamInstaller/Program.cs @@ -10,125 +10,124 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -namespace CreamInstaller +namespace CreamInstaller; + +internal static class Program { - internal static class Program + internal static readonly string ApplicationName = Application.CompanyName + " v" + Application.ProductVersion + ": " + Application.ProductName; + internal static readonly Assembly EntryAssembly = Assembly.GetEntryAssembly(); + internal static readonly Process CurrentProcess = Process.GetCurrentProcess(); + internal static readonly string CurrentProcessFilePath = CurrentProcess.MainModule.FileName; + internal static readonly string CurrentProcessDirectory = CurrentProcessFilePath.Substring(0, CurrentProcessFilePath.LastIndexOf("\\")); + internal static readonly string BackupFileExtension = ".creaminstaller.backup"; + + internal static bool BlockProtectedGames = true; + internal static readonly string[] ProtectedGameNames = { "PAYDAY 2", "Call to Arms" }; // non-functioning CreamAPI or DLL detections + internal static readonly string[] ProtectedGameDirectories = { @"\EasyAntiCheat", @"\BattlEye" }; // DLL detections + internal static readonly string[] ProtectedGameDirectoryExceptions = { "Arma 3" }; // Arma 3's BattlEye doesn't detect DLL changes? + + internal static bool IsGameBlocked(string name, string directory) { - internal static readonly string ApplicationName = Application.CompanyName + " v" + Application.ProductVersion + ": " + Application.ProductName; - internal static readonly Assembly EntryAssembly = Assembly.GetEntryAssembly(); - internal static readonly Process CurrentProcess = Process.GetCurrentProcess(); - internal static readonly string CurrentProcessFilePath = CurrentProcess.MainModule.FileName; - internal static readonly string CurrentProcessDirectory = CurrentProcessFilePath.Substring(0, CurrentProcessFilePath.LastIndexOf("\\")); - internal static readonly string BackupFileExtension = ".creaminstaller.backup"; + if (ProtectedGameNames.Contains(name)) return true; + if (!ProtectedGameDirectoryExceptions.Contains(name)) + foreach (string path in ProtectedGameDirectories) + if (Directory.Exists(directory + path)) return true; + return false; + } - internal static bool BlockProtectedGames = true; - internal static readonly string[] ProtectedGameNames = { "PAYDAY 2", "Call to Arms" }; // non-functioning CreamAPI or DLL detections - internal static readonly string[] ProtectedGameDirectories = { @"\EasyAntiCheat", @"\BattlEye" }; // DLL detections - internal static readonly string[] ProtectedGameDirectoryExceptions = { "Arma 3" }; // Arma 3's BattlEye doesn't detect DLL changes? - - internal static bool IsGameBlocked(string name, string directory) - { - if (ProtectedGameNames.Contains(name)) return true; - if (!ProtectedGameDirectoryExceptions.Contains(name)) - foreach (string path in ProtectedGameDirectories) - if (Directory.Exists(directory + path)) return true; - return false; - } - - [STAThread] - private static void Main() - { - Mutex mutex = new(true, "CreamInstaller", out bool createdNew); - if (createdNew) - { - Application.SetHighDpiMode(HighDpiMode.SystemAware); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.ApplicationExit += new(OnApplicationExit); - retry: - try - { - Application.Run(new MainForm()); - } - catch (Exception e) - { - if (ExceptionHandler.OutputException(e)) goto retry; - Application.Exit(); - return; - } - } - mutex.Close(); - } - - internal static async Task GetImageFromUrl(string url) + [STAThread] + private static void Main() + { + Mutex mutex = new(true, "CreamInstaller", out bool createdNew); + if (createdNew) { + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.ApplicationExit += new(OnApplicationExit); + retry: try { - HttpClient httpClient = new(); - httpClient.DefaultRequestHeaders.Add("user-agent", "CreamInstaller"); - return new Bitmap(await httpClient.GetStreamAsync(url)); + Application.Run(new MainForm()); } - catch { } - return null; - } - - internal static void OpenDirectoryInFileExplorer(string path) => Process.Start(new ProcessStartInfo - { - FileName = "explorer.exe", - Arguments = path - }); - - internal static void OpenUrlInInternetBrowser(string url) => Process.Start(new ProcessStartInfo - { - FileName = url, - UseShellExecute = true - }); - - internal static Image GetFileIconImage(string path) => File.Exists(path) ? Icon.ExtractAssociatedIcon(path).ToBitmap() : null; - - internal static Image GetFileExplorerImage() => GetFileIconImage(Environment.GetFolderPath(Environment.SpecialFolder.Windows) + @"\explorer.exe"); - - internal static bool IsProgramRunningDialog(Form form, ProgramSelection selection) - { - if (selection.AreSteamApiDllsLocked) + catch (Exception e) { - if (new DialogForm(form).Show(ApplicationName, SystemIcons.Error, - $"ERROR: {selection.Name} is currently running!" + - "\n\nPlease close the program/game to continue . . . ", - "Retry", "Cancel") == DialogResult.OK) - return IsProgramRunningDialog(form, selection); + if (ExceptionHandler.OutputException(e)) goto retry; + Application.Exit(); + return; } - else return true; - return false; - } - - internal static bool IsFilePathLocked(this string filePath) - { - try { File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None).Close(); } - catch (FileNotFoundException) { return false; } - catch (IOException) { return true; } - return false; - } - - internal static SelectForm SelectForm; - internal static InstallForm InstallForm; - - internal static List ProgramSelections = new(); - - internal static bool Canceled = false; - internal static async void Cleanup(bool cancel = true) - { - Canceled = cancel; - await SteamCMD.Kill(); - } - - private static void OnApplicationExit(object s, EventArgs e) => Cleanup(); - - internal static void InheritLocation(this Form form, Form fromForm) - { - int X = fromForm.Location.X + fromForm.Size.Width / 2 - form.Size.Width / 2; - int Y = fromForm.Location.Y + fromForm.Size.Height / 2 - form.Size.Height / 2; - form.Location = new(X, Y); } + mutex.Close(); } -} \ No newline at end of file + + internal static async Task GetImageFromUrl(string url) + { + try + { + HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.Add("user-agent", "CreamInstaller"); + return new Bitmap(await httpClient.GetStreamAsync(url)); + } + catch { } + return null; + } + + internal static void OpenDirectoryInFileExplorer(string path) => Process.Start(new ProcessStartInfo + { + FileName = "explorer.exe", + Arguments = path + }); + + internal static void OpenUrlInInternetBrowser(string url) => Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + + internal static Image GetFileIconImage(string path) => File.Exists(path) ? Icon.ExtractAssociatedIcon(path).ToBitmap() : null; + + internal static Image GetFileExplorerImage() => GetFileIconImage(Environment.GetFolderPath(Environment.SpecialFolder.Windows) + @"\explorer.exe"); + + internal static bool IsProgramRunningDialog(Form form, ProgramSelection selection) + { + if (selection.AreSteamApiDllsLocked) + { + if (new DialogForm(form).Show(ApplicationName, SystemIcons.Error, + $"ERROR: {selection.Name} is currently running!" + + "\n\nPlease close the program/game to continue . . . ", + "Retry", "Cancel") == DialogResult.OK) + return IsProgramRunningDialog(form, selection); + } + else return true; + return false; + } + + internal static bool IsFilePathLocked(this string filePath) + { + try { File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None).Close(); } + catch (FileNotFoundException) { return false; } + catch (IOException) { return true; } + return false; + } + + internal static SelectForm SelectForm; + internal static InstallForm InstallForm; + + internal static List ProgramSelections = new(); + + internal static bool Canceled = false; + internal static async void Cleanup(bool cancel = true) + { + Canceled = cancel; + await SteamCMD.Kill(); + } + + private static void OnApplicationExit(object s, EventArgs e) => Cleanup(); + + internal static void InheritLocation(this Form form, Form fromForm) + { + int X = fromForm.Location.X + fromForm.Size.Width / 2 - form.Size.Width / 2; + int Y = fromForm.Location.Y + fromForm.Size.Height / 2 - form.Size.Height / 2; + form.Location = new(X, Y); + } +} diff --git a/CreamInstaller/Resources/FileResourceExtensions.cs b/CreamInstaller/Resources/FileResourceExtensions.cs index c0c43fb..018689a 100644 --- a/CreamInstaller/Resources/FileResourceExtensions.cs +++ b/CreamInstaller/Resources/FileResourceExtensions.cs @@ -1,13 +1,12 @@ using System.IO; -namespace CreamInstaller +namespace CreamInstaller; + +internal static class FileResourceExtensions { - internal static class FileResourceExtensions + internal static void Write(this byte[] resource, string filePath) { - internal static void Write(this byte[] resource, string filePath) - { - using FileStream file = new(filePath, FileMode.Create, FileAccess.Write); - file.Write(resource); - } + using FileStream file = new(filePath, FileMode.Create, FileAccess.Write); + file.Write(resource); } -} \ No newline at end of file +}