diff --git a/CreamInstaller/Classes/HttpClientManager.cs b/CreamInstaller/Classes/HttpClientManager.cs deleted file mode 100644 index 1a3826a..0000000 --- a/CreamInstaller/Classes/HttpClientManager.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -using HtmlAgilityPack; - -namespace CreamInstaller.Classes; - -internal static class HttpClientManager -{ - private static HttpClient httpClient; - internal static void Setup() - { - httpClient = new(); - httpClient.DefaultRequestHeaders.Add("user-agent", $"CreamInstaller-{Environment.MachineName}_{Environment.UserDomainName}_{Environment.UserName}"); - } - - internal static async Task GetDocumentNodes(string url, string xpath) - { - try - { - 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); - HtmlDocument document = new(); - document.LoadHtml(reader.ReadToEnd()); - return document.DocumentNode.SelectNodes(xpath); - } - catch { return null; } - } - - internal static async Task GetImageFromUrl(string url) - { - try - { - return new Bitmap(await httpClient.GetStreamAsync(url)); - } - catch { } - return null; - } - - internal static async Task ParseSteamStoreDlcAppIds(int appId, List dlcIds) - { - // currently this is only really needed to get DLC that release without changing game buildid (very rare) - // it also finds things which aren't really connected to the game itself, and thus not needed (usually soundtracks, collections, packs, etc.) - HtmlNodeCollection nodes = await GetDocumentNodes( - $"https://store.steampowered.com/dlc/{appId}", - "//div[@class='recommendation']/div/a"); - if (nodes is not null) - foreach (HtmlNode node in nodes) - if (int.TryParse(node.Attributes?["data-ds-appid"]?.Value, out int dlcAppId) && dlcAppId > 0 && !dlcIds.Contains(dlcAppId)) - dlcIds.Add(dlcAppId); - } - - internal static void Dispose() => httpClient.Dispose(); -} diff --git a/CreamInstaller/Classes/ProgramSelection.cs b/CreamInstaller/Classes/ProgramSelection.cs deleted file mode 100644 index 9b5ddf1..0000000 --- a/CreamInstaller/Classes/ProgramSelection.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using Gameloop.Vdf.Linq; - -namespace CreamInstaller.Classes; - -internal class ProgramSelection -{ - internal bool Enabled = false; - internal bool Usable = true; - - internal int SteamAppId = 0; - internal string Name = "Program"; - - internal string IconStaticID = null; - internal string ClientIconStaticID = null; - - 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) - { - directory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); - if (api.IsFilePathLocked() - || api_o.IsFilePathLocked() - || api64.IsFilePathLocked() - || api64_o.IsFilePathLocked() - || cApi.IsFilePathLocked()) - return true; - } - return false; - } - } - - private void Toggle(int dlcAppId, (string name, string iconStaticId) dlcApp, bool enabled) - { - if (enabled) SelectedSteamDlc[dlcAppId] = dlcApp; - else SelectedSteamDlc.Remove(dlcAppId); - } - - internal void ToggleDlc(int dlcAppId, bool enabled) - { - foreach (KeyValuePair pair in AllSteamDlc) - { - int appId = pair.Key; - (string name, string iconStaticId) dlcApp = pair.Value; - if (appId == dlcAppId) - { - Toggle(appId, dlcApp, enabled); - break; - } - } - Enabled = SelectedSteamDlc.Any(); - } - - internal void ToggleAllDlc(bool enabled) - { - if (!enabled) SelectedSteamDlc.Clear(); - else foreach (KeyValuePair pair in AllSteamDlc) - { - int appId = pair.Key; - (string name, string iconStaticId) dlcApp = pair.Value; - Toggle(appId, dlcApp, enabled); - } - Enabled = SelectedSteamDlc.Any(); - } - - internal ProgramSelection() => All.Add(this); - - internal void Validate() - { - if (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 = new(); - - 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 (int gameAppId, (string name, string iconStaticId) app)? GetDlcFromAppId(int appId) - { - foreach (ProgramSelection selection in AllUsable) - foreach (KeyValuePair pair in selection.AllSteamDlc) - if (pair.Key == appId) return (selection.SteamAppId, pair.Value); - return null; - } -} diff --git a/CreamInstaller/CreamInstaller.csproj b/CreamInstaller/CreamInstaller.csproj index f892847..29ce3de 100644 --- a/CreamInstaller/CreamInstaller.csproj +++ b/CreamInstaller/CreamInstaller.csproj @@ -5,21 +5,21 @@ True Resources\ini.ico true - 2.5.0.1 + 3.0.0.0 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. + LICENSE 2021, pointfeev (https://github.com/pointfeev) true https://github.com/pointfeev/CreamInstaller https://github.com/pointfeev/CreamInstaller git - Automatically downloads and installs CreamAPI files for programs/games. + steam, dlc CreamInstaller CreamInstaller - CreamAPI Generator & Installer + CreamAPI/ScreamAPI Installer & Configuration Generator pointfeev pointfeev.creaminstaller CreamInstaller.Program @@ -44,6 +44,7 @@ + diff --git a/CreamInstaller/Epic/EpicLibrary.cs b/CreamInstaller/Epic/EpicLibrary.cs new file mode 100644 index 0000000..9b04164 --- /dev/null +++ b/CreamInstaller/Epic/EpicLibrary.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.Win32; + +namespace CreamInstaller.Epic; + +internal static class EpicLibrary +{ + private static string epicAppDataPath = null; + internal static string EpicAppDataPath + { + get + { + epicAppDataPath ??= Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Epic Games\EpicGamesLauncher", "AppDataPath", null) as string; + epicAppDataPath ??= Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Epic Games\EpicGamesLauncher", "AppDataPath", null) as string; + return epicAppDataPath; + } + } + + internal static async Task> GetGames() => await Task.Run(() => + { + List games = new(); + if (!Directory.Exists(EpicAppDataPath)) return games; + string manifests = EpicAppDataPath + @"\Manifests"; + if (!Directory.Exists(manifests)) return games; + string[] files = Directory.GetFiles(manifests); + foreach (string file in files) + { + if (Program.Canceled) return games; + if (Path.GetExtension(file) == ".item") + { + string json = File.ReadAllText(file); + try + { + Manifest manifest = JsonSerializer.Deserialize(json); + if (manifest is not null && manifest.CatalogItemId == manifest.MainGameCatalogItemId) + games.Add(manifest); + } + catch { }; + } + } + return games; + }); + + internal static async Task> GetDllDirectoriesFromGameDirectory(string gameDirectory) => await Task.Run(async () => + { + List dllDirectories = new(); + if (Program.Canceled || !Directory.Exists(gameDirectory)) return null; + gameDirectory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (File.Exists(sdk) + || File.Exists(sdk_o) + || File.Exists(sdk64) + || File.Exists(sdk64_o) + || File.Exists(sApi)) + 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 { } + } + return !dllDirectories.Any() ? null : dllDirectories; + }); +} \ No newline at end of file diff --git a/CreamInstaller/Epic/EpicStore.cs b/CreamInstaller/Epic/EpicStore.cs new file mode 100644 index 0000000..d1c41b0 --- /dev/null +++ b/CreamInstaller/Epic/EpicStore.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Web; + +using CreamInstaller.Epic.GraphQL; +using CreamInstaller.Utility; + +using Newtonsoft.Json; + +namespace CreamInstaller.Epic; + +internal static class EpicStore +{ + internal static async Task> ParseDlcAppIds(string categoryNamespace) + { + List<(string id, string name)> dlcIds = new(); + Response response = await QueryEpicGraphQL(categoryNamespace); + if (response is null) + return dlcIds; + List elements = new(response.Data.Catalog.CatalogOffers.Elements); + elements.AddRange(response.Data.Catalog.SearchStore.Elements); + foreach (Element element in elements) + { + (string id, string name) app = (element.Items[0].Id, element.Title); + if (!dlcIds.Contains(app)) + dlcIds.Add(app); + } + return dlcIds; + } + + internal static async Task QueryEpicGraphQL(string categoryNamespace) + { + string encoded = HttpUtility.UrlEncode(categoryNamespace); + Request request = new(encoded); + string payload = JsonConvert.SerializeObject(request); + HttpContent content = new StringContent(payload); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + HttpClient client = HttpClientManager.HttpClient; + if (client is null) return null; + HttpResponseMessage httpResponse = await client.PostAsync("https://graphql.epicgames.com/graphql", content); + httpResponse.EnsureSuccessStatusCode(); + string response = await httpResponse.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(response); + } +} diff --git a/CreamInstaller/Epic/GraphQL/Request.cs b/CreamInstaller/Epic/GraphQL/Request.cs new file mode 100644 index 0000000..1b05624 --- /dev/null +++ b/CreamInstaller/Epic/GraphQL/Request.cs @@ -0,0 +1,94 @@ + +using Newtonsoft.Json; + +namespace CreamInstaller.Epic.GraphQL; + +internal class Request +{ +#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable IDE0052 // Remove unread private members +#pragma warning disable IDE1006 // Naming Styles + + [JsonProperty(PropertyName = "query")] + private string _gqlQuery => @"query searchOffers($namespace: String!) { + Catalog { + catalogOffers( + namespace: $namespace + params: { + count: 1000, + } + ) { + elements { + id + title + offerType + items { + id + } + keyImages { + type + url + } + catalogNs { + mappings(pageType: ""productHome"") { + pageSlug + } + } + } + } + searchStore(category: ""games/edition/base"", namespace: $namespace) { + elements { + id + title + offerType + items { + id + } + keyImages { + type + url + } + catalogNs { + mappings(pageType: ""productHome"") { + pageSlug + } + } + } + } + } +}"; + + [JsonProperty(PropertyName = "variables")] + private Variables _variables { get; set; } + + internal Request(string _namespace) => _variables = new Variables(_namespace); + + private class Headers + { + [JsonProperty(PropertyName = "Content-Type")] + private string _contentType => "application/graphql"; + } + + private class Variables + { + [JsonProperty(PropertyName = "category")] + private string _category => "games/edition/base|bundles/games|editors|software/edition/base"; + + [JsonProperty(PropertyName = "count")] + private int _count => 1000; + + [JsonProperty(PropertyName = "keywords")] + private string _keywords => ""; + + [JsonProperty(PropertyName = "namespace")] + private string _namespace { get; set; } + + internal Variables(string _namespace) => this._namespace = _namespace; + } + +#pragma warning restore IDE0051 // Remove unused private members +#pragma warning restore CA1822 // Mark members as static +#pragma warning restore IDE0052 // Remove unread private members +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/CreamInstaller/Epic/GraphQL/Response.cs b/CreamInstaller/Epic/GraphQL/Response.cs new file mode 100644 index 0000000..aba32d9 --- /dev/null +++ b/CreamInstaller/Epic/GraphQL/Response.cs @@ -0,0 +1,95 @@ +using System; + +using Newtonsoft.Json; + +namespace CreamInstaller.Epic.GraphQL; + +public class Response +{ + [JsonProperty(PropertyName = "data")] + public Data Data { get; protected set; } +} + +public class Data +{ + [JsonProperty(PropertyName = "Catalog")] + public Catalog Catalog { get; protected set; } +} + +public class Catalog +{ + [JsonProperty(PropertyName = "catalogOffers")] + public SearchStore CatalogOffers { get; protected set; } + + [JsonProperty(PropertyName = "searchStore")] + public SearchStore SearchStore { get; protected set; } +} + +public class CatalogOffers +{ + [JsonProperty(PropertyName = "namespace")] + public string Namespace { get; protected set; } + + [JsonProperty(PropertyName = "params")] + public Parameters Parameters { get; protected set; } +} + +public class Parameters +{ + [JsonProperty(PropertyName = "count")] + public int Count { get; protected set; } +} + +public class SearchStore +{ + [JsonProperty(PropertyName = "elements")] + public Element[] Elements { get; protected set; } +} + +public class Element +{ + [JsonProperty(PropertyName = "id")] + public string Id { get; protected set; } + + [JsonProperty(PropertyName = "title")] + public string Title { get; protected set; } + + [JsonProperty(PropertyName = "offerType")] + public string OfferType { get; protected set; } + + [JsonProperty(PropertyName = "items")] + public Item[] Items { get; protected set; } + + [JsonProperty(PropertyName = "keyImages")] + public KeyImage[] KeyImages { get; protected set; } + + [JsonProperty(PropertyName = "catalogNs")] + public CatalogNs CatalogNs { get; protected set; } +} + +public class Item +{ + [JsonProperty(PropertyName = "id")] + public string Id { get; protected set; } +} + +public class KeyImage +{ + [JsonProperty(PropertyName = "type")] + public string Type { get; protected set; } + + [JsonProperty(PropertyName = "url")] + public string Url { get; protected set; } +} + +public class CatalogNs +{ + [JsonProperty(PropertyName = "mappings")] + public Mapping[] Mappings { get; protected set; } +} + +public class Mapping +{ + [JsonProperty(PropertyName = "pageSlug")] + public string PageSlug { get; protected set; } +} \ No newline at end of file diff --git a/CreamInstaller/Epic/Manifest.cs b/CreamInstaller/Epic/Manifest.cs new file mode 100644 index 0000000..39508d4 --- /dev/null +++ b/CreamInstaller/Epic/Manifest.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace CreamInstaller.Epic; + +public class Manifest +{ +#pragma warning disable IDE1006 // Naming Styles + public int FormatVersion { get; set; } + public bool bIsIncompleteInstall { get; set; } + public string LaunchCommand { get; set; } + public string LaunchExecutable { get; set; } + public string ManifestLocation { get; set; } + public bool bIsApplication { get; set; } + public bool bIsExecutable { get; set; } + public bool bIsManaged { get; set; } + public bool bNeedsValidation { get; set; } + public bool bRequiresAuth { get; set; } + public bool bAllowMultipleInstances { get; set; } + public bool bCanRunOffline { get; set; } + public bool bAllowUriCmdArgs { get; set; } + public List BaseURLs { get; set; } + public string BuildLabel { get; set; } + public List AppCategories { get; set; } + public List ChunkDbs { get; set; } + public List CompatibleApps { get; set; } + public string DisplayName { get; set; } + public string InstallationGuid { get; set; } + public string InstallLocation { get; set; } + public string InstallSessionId { get; set; } + public List InstallTags { get; set; } + public List InstallComponents { get; set; } + public string HostInstallationGuid { get; set; } + public List PrereqIds { get; set; } + public string StagingLocation { get; set; } + public string TechnicalType { get; set; } + public string VaultThumbnailUrl { get; set; } + public string VaultTitleText { get; set; } + public long InstallSize { get; set; } + public string MainWindowProcessName { get; set; } + public List ProcessNames { get; set; } + public List BackgroundProcessNames { get; set; } + public string MandatoryAppFolderName { get; set; } + public string OwnershipToken { get; set; } + public string CatalogNamespace { get; set; } + public string CatalogItemId { get; set; } + public string AppName { get; set; } + public string AppVersionString { get; set; } + public string MainGameCatalogNamespace { get; set; } + public string MainGameCatalogItemId { get; set; } + public string MainGameAppName { get; set; } + public List AllowedUriEnvVars { get; set; } +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/CreamInstaller/Forms/Components/CustomTreeView.cs b/CreamInstaller/Forms/Components/CustomTreeView.cs index d29b8ec..2b27db4 100644 --- a/CreamInstaller/Forms/Components/CustomTreeView.cs +++ b/CreamInstaller/Forms/Components/CustomTreeView.cs @@ -34,7 +34,7 @@ internal class CustomTreeView : TreeView 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) + if (subText == "ParadoxLauncher") return; Size subSize = TextRenderer.MeasureText(graphics, subText, subFont); diff --git a/CreamInstaller/Forms/DialogForm.cs b/CreamInstaller/Forms/DialogForm.cs index 3b569b7..2a23509 100644 --- a/CreamInstaller/Forms/DialogForm.cs +++ b/CreamInstaller/Forms/DialogForm.cs @@ -1,4 +1,5 @@ -using System.Drawing; +using System; +using System.Drawing; using System.Windows.Forms; using CreamInstaller.Forms.Components; @@ -9,12 +10,11 @@ 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 customFormIcon = null) + internal DialogResult Show(Icon descriptionIcon, string descriptionText, string acceptButtonText, string cancelButtonText = null, Icon customFormIcon = null) { if (customFormIcon is not null) Icon = customFormIcon; icon.Image = descriptionIcon.ToBitmap(); - Text = formName; descriptionLabel.Text = descriptionText; acceptButton.Text = acceptButtonText; if (cancelButtonText is null) @@ -23,6 +23,13 @@ internal partial class DialogForm : CustomForm cancelButton.Visible = false; } else cancelButton.Text = cancelButtonText; + OnResize(null, null); + Resize += OnResize; return ShowDialog(); } + + internal void OnResize(object s, EventArgs e) => + Text = TextRenderer.MeasureText(Program.ApplicationName, Font).Width > Size.Width - 100 + ? Program.ApplicationNameShort + : Program.ApplicationName; } diff --git a/CreamInstaller/Forms/InstallForm.cs b/CreamInstaller/Forms/InstallForm.cs index 2e62582..8545abd 100644 --- a/CreamInstaller/Forms/InstallForm.cs +++ b/CreamInstaller/Forms/InstallForm.cs @@ -7,9 +7,9 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Forms; -using CreamInstaller.Classes; using CreamInstaller.Forms.Components; using CreamInstaller.Resources; +using CreamInstaller.Utility; namespace CreamInstaller; @@ -53,9 +53,8 @@ internal partial class InstallForm : CustomForm } } - internal static void WriteConfiguration(StreamWriter writer, int steamAppId, string name, SortedList steamDlcApps, InstallForm installForm = null) + internal static void WriteCreamConfiguration(StreamWriter writer, string steamAppId, string name, SortedList steamDlcApps, InstallForm installForm = null) { - writer.WriteLine(); writer.WriteLine($"; {name}"); writer.WriteLine("[steam]"); writer.WriteLine($"appid = {steamAppId}"); @@ -63,19 +62,19 @@ internal partial class InstallForm : CustomForm writer.WriteLine("[dlc]"); if (installForm is not null) installForm.UpdateUser($"Added game to cream_api.ini with appid {steamAppId} ({name})", InstallationLog.Resource, info: false); - foreach (KeyValuePair pair in steamDlcApps) + foreach (KeyValuePair pair in steamDlcApps) { - int appId = pair.Key; - (string name, string iconStaticId) dlcApp = pair.Value; - writer.WriteLine($"{appId} = {dlcApp.name}"); + string appId = pair.Key; + (string dlcName, _) = pair.Value; + writer.WriteLine($"{appId} = {dlcName}"); if (installForm is not null) - installForm.UpdateUser($"Added DLC to cream_api.ini with appid {appId} ({dlcApp.name})", InstallationLog.Resource, info: false); + installForm.UpdateUser($"Added DLC to cream_api.ini with appid {appId} ({dlcName})", InstallationLog.Resource, info: false); } } internal static async Task UninstallCreamAPI(string directory, InstallForm installForm = null) => await Task.Run(() => { - directory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + directory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); if (File.Exists(api_o)) { if (File.Exists(api)) @@ -110,7 +109,7 @@ internal partial class InstallForm : CustomForm internal static async Task InstallCreamAPI(string directory, ProgramSelection selection, InstallForm installForm = null) => await Task.Run(() => { - directory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + directory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); if (File.Exists(api) && !File.Exists(api_o)) { File.Move(api, api_o); @@ -136,14 +135,125 @@ internal partial class InstallForm : CustomForm installForm.UpdateUser($"Wrote resource to file: {Path.GetFileName(api64)}", InstallationLog.Resource, info: false); } if (installForm is not null) - installForm.UpdateUser("Generating CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); + installForm.UpdateUser("Generating CreamAPI configuration 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) - WriteConfiguration(writer, selection.SteamAppId, selection.Name, selection.SelectedSteamDlc, installForm); - foreach (Tuple> extraAppDlc in selection.ExtraSteamAppIdDlc) - WriteConfiguration(writer, extraAppDlc.Item1, extraAppDlc.Item2, extraAppDlc.Item3, installForm); + if (selection.Id != "ParadoxLauncher") + WriteCreamConfiguration(writer, selection.Id, selection.Name, selection.SelectedDlc, installForm); + foreach (Tuple> extraAppDlc in selection.ExtraDlc) + WriteCreamConfiguration(writer, extraAppDlc.Item1, extraAppDlc.Item2, extraAppDlc.Item3, installForm); + writer.Flush(); + writer.Close(); + }); + + internal static void WriteScreamConfiguration(StreamWriter writer, SortedList steamDlcApps, InstallForm installForm = null) + { + writer.WriteLine("{"); + writer.WriteLine(" \"version\": 2,"); + writer.WriteLine(" \"logging\": false,"); + writer.WriteLine(" \"eos_logging\": false,"); + writer.WriteLine(" \"block_metrics\": false,"); + writer.WriteLine(" \"catalog_items\": {"); + writer.WriteLine(" \"unlock_all\": false,"); + writer.WriteLine(" \"override\": ["); + KeyValuePair last = steamDlcApps.Last(); + foreach (KeyValuePair pair in steamDlcApps) + { + string id = pair.Key; + (string name, _) = pair.Value; + writer.WriteLine($" \"{id}\"{(pair.Equals(last) ? "" : ",")}"); + if (installForm is not null) + installForm.UpdateUser($"Added DLC to ScreamAPI.json with id {id} ({name})", InstallationLog.Resource, info: false); + } + writer.WriteLine(" ]"); + writer.WriteLine(" },"); + writer.WriteLine(" \"entitlements\": {"); + writer.WriteLine(" \"unlock_all\": false,"); + writer.WriteLine(" \"auto_inject\": false,"); + writer.WriteLine(" \"inject\": ["); + foreach (KeyValuePair pair in steamDlcApps) + { + string id = pair.Key; + (string name, _) = pair.Value; + writer.WriteLine($" \"{id}\"{(pair.Equals(last) ? "" : ",")}"); + if (installForm is not null) + installForm.UpdateUser($"Added DLC to ScreamAPI.json with id {id} ({name})", InstallationLog.Resource, info: false); + } + writer.WriteLine(" ]"); + writer.WriteLine(" }"); + writer.WriteLine("}"); + } + + internal static async Task UninstallScreamAPI(string directory, InstallForm installForm = null) => await Task.Run(() => + { + directory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (File.Exists(sdk_o)) + { + if (File.Exists(sdk)) + { + File.Delete(sdk); + if (installForm is not null) + installForm.UpdateUser($"Deleted file: {Path.GetFileName(sdk)}", InstallationLog.Resource, info: false); + } + File.Move(sdk_o, sdk); + if (installForm is not null) + installForm.UpdateUser($"Renamed file: {Path.GetFileName(sdk_o)} -> {Path.GetFileName(sdk)}", InstallationLog.Resource, info: false); + } + if (File.Exists(sdk64_o)) + { + if (File.Exists(sdk64)) + { + File.Delete(sdk64); + if (installForm is not null) + installForm.UpdateUser($"Deleted file: {Path.GetFileName(sdk64)}", InstallationLog.Resource, info: false); + } + File.Move(sdk64_o, sdk64); + if (installForm is not null) + installForm.UpdateUser($"Renamed file: {Path.GetFileName(sdk64_o)} -> {Path.GetFileName(sdk64)}", InstallationLog.Resource, info: false); + } + if (File.Exists(sApi)) + { + File.Delete(sApi); + if (installForm is not null) + installForm.UpdateUser($"Deleted file: {Path.GetFileName(sApi)}", InstallationLog.Resource, info: false); + } + }); + + internal static async Task InstallScreamAPI(string directory, ProgramSelection selection, InstallForm installForm = null) => await Task.Run(() => + { + directory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (File.Exists(sdk) && !File.Exists(sdk_o)) + { + File.Move(sdk, sdk_o); + if (installForm is not null) + installForm.UpdateUser($"Renamed file: {Path.GetFileName(sdk)} -> {Path.GetFileName(sdk_o)}", InstallationLog.Resource, info: false); + } + if (File.Exists(sdk_o)) + { + Properties.Resources.SDK.Write(sdk); + if (installForm is not null) + installForm.UpdateUser($"Wrote resource to file: {Path.GetFileName(sdk)}", InstallationLog.Resource, info: false); + } + if (File.Exists(sdk64) && !File.Exists(sdk64_o)) + { + File.Move(sdk64, sdk64_o); + if (installForm is not null) + installForm.UpdateUser($"Renamed file: {Path.GetFileName(sdk64)} -> {Path.GetFileName(sdk64_o)}", InstallationLog.Resource, info: false); + } + if (File.Exists(sdk64_o)) + { + Properties.Resources.SDK64.Write(sdk64); + if (installForm is not null) + installForm.UpdateUser($"Wrote resource to file: {Path.GetFileName(sdk64)}", InstallationLog.Resource, info: false); + } + if (installForm is not null) + installForm.UpdateUser("Generating ScreamAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); + File.Create(sApi).Close(); + StreamWriter writer = new(sApi, true, Encoding.UTF8); + if (selection.Id != "ParadoxLauncher") + WriteScreamConfiguration(writer, selection.SelectedDlc, installForm); + foreach (Tuple> extraAppDlc in selection.ExtraDlc) + WriteScreamConfiguration(writer, extraAppDlc.Item3, installForm); writer.Flush(); writer.Close(); }); @@ -151,16 +261,27 @@ internal partial class InstallForm : CustomForm private async Task OperateFor(ProgramSelection selection) { UpdateProgress(0); - int count = selection.SteamApiDllDirectories.Count; + int count = selection.DllDirectories.Count; int cur = 0; - foreach (string directory in selection.SteamApiDllDirectories) + foreach (string directory in selection.DllDirectories) { - UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} CreamAPI for " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); + UpdateUser($"{(Uninstalling ? "Uninstalling" : "Installing")} {(selection.IsSteam ? "CreamAPI" : "ScreamAPI")}" + + $" {(Uninstalling ? "from" : "for")} " + selection.Name + $" in directory \"{directory}\" . . . ", InstallationLog.Operation); if (!Program.IsProgramRunningDialog(this, selection)) throw new OperationCanceledException(); - if (Uninstalling) - await UninstallCreamAPI(directory, this); + if (selection.IsSteam) + { + if (Uninstalling) + await UninstallCreamAPI(directory, this); + else + await InstallCreamAPI(directory, selection, this); + } else - await InstallCreamAPI(directory, selection, this); + { + if (Uninstalling) + await UninstallScreamAPI(directory, this); + else + await InstallScreamAPI(directory, selection, this); + } UpdateProgress(++cur / count * 100); } UpdateProgress(100); @@ -208,11 +329,11 @@ internal partial class InstallForm : CustomForm try { await Operate(); - UpdateUser($"CreamAPI successfully {(Uninstalling ? "uninstalled" : "installed and generated")} for " + ProgramCount + " program(s).", InstallationLog.Success); + UpdateUser($"CreamAPI/ScreamAPI successfully {(Uninstalling ? "uninstalled" : "installed and generated")} for " + ProgramCount + " program(s).", InstallationLog.Success); } catch (Exception exception) { - UpdateUser($"CreamAPI {(Uninstalling ? "uninstallation" : "installation and/or generation")} failed: " + exception.ToString(), InstallationLog.Error); + UpdateUser($"CreamAPI/ScreamAPI {(Uninstalling ? "uninstallation" : "installation and/or generation")} failed: " + exception.ToString(), InstallationLog.Error); retryButton.Enabled = true; } userProgressBar.Value = userProgressBar.Maximum; diff --git a/CreamInstaller/Forms/MainForm.cs b/CreamInstaller/Forms/MainForm.cs index 9589758..c121079 100644 --- a/CreamInstaller/Forms/MainForm.cs +++ b/CreamInstaller/Forms/MainForm.cs @@ -7,8 +7,8 @@ using System.Threading.Tasks; using System.Web; using System.Windows.Forms; -using CreamInstaller.Classes; using CreamInstaller.Forms.Components; +using CreamInstaller.Utility; using HtmlAgilityPack; @@ -23,7 +23,7 @@ internal partial class MainForm : CustomForm internal MainForm() : base() { InitializeComponent(); - Text = Program.ApplicationName; + Text = Program.ApplicationNameShort; } private static CancellationTokenSource cancellationTokenSource; @@ -127,7 +127,7 @@ internal partial class MainForm : CustomForm { string FileName = Path.GetFileName(Program.CurrentProcessFilePath); if (FileName != "CreamInstaller.exe") - if (new DialogForm(this).Show(Program.ApplicationName, SystemIcons.Warning, + if (new DialogForm(this).Show(SystemIcons.Warning, "WARNING: CreamInstaller.exe was renamed!" + "\n\nThis will cause unwanted behavior when updating the program!", "Ignore", "Abort") == DialogResult.Cancel) diff --git a/CreamInstaller/Forms/SelectForm.Designer.cs b/CreamInstaller/Forms/SelectForm.Designer.cs index c7480c8..5704576 100644 --- a/CreamInstaller/Forms/SelectForm.Designer.cs +++ b/CreamInstaller/Forms/SelectForm.Designer.cs @@ -113,7 +113,7 @@ namespace CreamInstaller this.noneFoundLabel.Name = "noneFoundLabel"; this.noneFoundLabel.Size = new System.Drawing.Size(554, 218); this.noneFoundLabel.TabIndex = 1002; - this.noneFoundLabel.Text = "No CreamAPI-applicable programs were found on your computer!"; + this.noneFoundLabel.Text = "No CreamAPI-applicable or ScreamAPI-applicable programs were found on your computer!"; this.noneFoundLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; this.noneFoundLabel.Visible = false; // diff --git a/CreamInstaller/Forms/SelectForm.cs b/CreamInstaller/Forms/SelectForm.cs index 59bc8d1..15e87c3 100644 --- a/CreamInstaller/Forms/SelectForm.cs +++ b/CreamInstaller/Forms/SelectForm.cs @@ -9,9 +9,12 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using CreamInstaller.Classes; +using CreamInstaller.Epic; using CreamInstaller.Forms.Components; +using CreamInstaller.Paradox; using CreamInstaller.Resources; +using CreamInstaller.Steam; +using CreamInstaller.Utility; using Gameloop.Vdf.Linq; @@ -79,152 +82,256 @@ internal partial class SelectForm : CustomForm } internal readonly List RunningTasks = new(); - private async Task GetCreamApiApplicablePrograms(IProgress progress) + private async Task GetApplicablePrograms(IProgress progress) { if (Program.Canceled) return; - List> applicablePrograms = new(); - if (Directory.Exists(SteamLibrary.ParadoxLauncherInstallPath)) - applicablePrograms.Add(new(0, "Paradox Launcher", "", 0, SteamLibrary.ParadoxLauncherInstallPath)); - List gameLibraryDirectories = await SteamLibrary.GetLibraryDirectories(); - foreach (string libraryDirectory in gameLibraryDirectories) - { - List> games = await SteamLibrary.GetGamesFromLibraryDirectory(libraryDirectory); - if (games is not null) - foreach (Tuple game in games) - if (!applicablePrograms.Any(_game => _game.Item1 == game.Item1)) - applicablePrograms.Add(game); - } - int CompleteTasks = 0; RunningTasks.Clear(); // contains all running tasks including games AND their dlc RemainingGames.Clear(); // for display purposes only, otherwise ignorable RemainingDLCs.Clear(); // for display purposes only, otherwise ignorable List appTasks = new(); - foreach (Tuple program in applicablePrograms) + if (Directory.Exists(ParadoxLauncher.InstallPath)) { - 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.IsGameBlocked(name, directory)) continue; - AddToRemainingGames(name); - Task task = Task.Run(async () => + ProgramSelection selection = ProgramSelection.FromId("ParadoxLauncher"); + selection ??= new(); + if (allCheckBox.Checked) selection.Enabled = true; + selection.Usable = true; + selection.Id = "ParadoxLauncher"; + selection.Name = "Paradox Launcher"; + selection.RootDirectory = ParadoxLauncher.InstallPath; + List steamDllDirectories = await SteamLibrary.GetDllDirectoriesFromGameDirectory(selection.RootDirectory); + selection.DllDirectories = steamDllDirectories ?? await EpicLibrary.GetDllDirectoriesFromGameDirectory(selection.RootDirectory); + selection.IsSteam = steamDllDirectories is not null; + + TreeNode programNode = TreeNodes.Find(s => s.Name == selection.Id) ?? new(); + programNode.Name = selection.Id; + programNode.Text = selection.Name; + programNode.Checked = selection.Enabled; + programNode.Remove(); + selectionTreeView.Nodes.Add(programNode); + } + if (Directory.Exists(SteamLibrary.InstallPath)) + { + List> steamGames = await SteamLibrary.GetGames(); + foreach (Tuple program in steamGames) { + string appId = program.Item1; + string name = program.Item2; + string branch = program.Item3; + int buildId = program.Item4; + string directory = program.Item5; + ProgramSelection selection = ProgramSelection.FromId(appId); if (Program.Canceled) return; - List dllDirectories = await SteamLibrary.GetDllDirectoriesFromGameDirectory(directory); - if (dllDirectories is null) + if (Program.IsGameBlocked(name, directory)) continue; + AddToRemainingGames(name); + Task task = Task.Run(async () => { - RemoveFromRemainingGames(name); - return; - } - VProperty appInfo = null; - if (appId > 0) appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId); - if (appId > 0 && appInfo is null) - { - RemoveFromRemainingGames(name); - return; - } - if (Program.Canceled) return; - ConcurrentDictionary dlc = new(); - List dlcTasks = new(); - List dlcIds = await SteamCMD.ParseDlcAppIds(appInfo); - await HttpClientManager.ParseSteamStoreDlcAppIds(appId, dlcIds); - if (dlcIds.Count > 0) - { - foreach (int dlcAppId in dlcIds) + if (Program.Canceled) return; + List dllDirectories = await SteamLibrary.GetDllDirectoriesFromGameDirectory(directory); + if (dllDirectories is null) { - if (Program.Canceled) return; - AddToRemainingDLCs(dlcAppId.ToString()); - Task task = Task.Run(async () => + RemoveFromRemainingGames(name); + return; + } + VProperty appInfo = appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId); + if (appInfo is null) + { + RemoveFromRemainingGames(name); + return; + } + if (Program.Canceled) return; + ConcurrentDictionary dlc = new(); + List dlcTasks = new(); + List dlcIds = await SteamCMD.ParseDlcAppIds(appInfo); + await SteamStore.ParseDlcAppIds(appId, dlcIds); + if (dlcIds.Count > 0) + { + foreach (string dlcAppId in dlcIds) { if (Program.Canceled) return; - string dlcName = null; - string dlcIconStaticId = null; - VProperty dlcAppInfo = await SteamCMD.GetAppInfo(dlcAppId); - if (dlcAppInfo is not null) + AddToRemainingDLCs(dlcAppId); + Task task = Task.Run(async () => { - dlcName = dlcAppInfo.Value?.GetChild("common")?.GetChild("name")?.ToString(); - dlcIconStaticId = dlcAppInfo.Value?.GetChild("common")?.GetChild("icon")?.ToString(); - dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo_small")?.ToString(); - dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo")?.ToString(); - } - if (Program.Canceled) return; - if (!string.IsNullOrWhiteSpace(dlcName)) - dlc[dlcAppId] = (dlcName, dlcIconStaticId); - RemoveFromRemainingDLCs(dlcAppId.ToString()); - 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) - { - RemoveFromRemainingGames(name); - return; - } - if (Program.Canceled) return; - foreach (Task task in dlcTasks) - { - if (Program.Canceled) return; - await task; - } - - selection ??= new(); - selection.Usable = true; - selection.SteamAppId = appId; - selection.Name = name; - selection.RootDirectory = directory; - selection.SteamApiDllDirectories = dllDirectories; - selection.AppInfo = appInfo; - selection.IconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString(); - selection.ClientIconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString(); - if (allCheckBox.Checked) selection.Enabled = true; - - if (Program.Canceled) return; - Program.Invoke(selectionTreeView, 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? + if (Program.Canceled) return; + string dlcName = null; + string dlcIconStaticId = null; + VProperty dlcAppInfo = await SteamCMD.GetAppInfo(dlcAppId); + if (dlcAppInfo is not null) + { + dlcName = dlcAppInfo.Value?.GetChild("common")?.GetChild("name")?.ToString(); + dlcIconStaticId = dlcAppInfo.Value?.GetChild("common")?.GetChild("icon")?.ToString(); + dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo_small")?.ToString(); + dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo")?.ToString(); + } + if (Program.Canceled) return; + if (!string.IsNullOrWhiteSpace(dlcName)) + dlc[dlcAppId] = (dlcName, dlcIconStaticId); + RemoveFromRemainingDLCs(dlcAppId); + progress.Report(++CompleteTasks); + }); + dlcTasks.Add(task); + RunningTasks.Add(task); + progress.Report(-RunningTasks.Count); + Thread.Sleep(10); // to reduce control & window freezing + } } else { - foreach (KeyValuePair pair in dlc) + RemoveFromRemainingGames(name); + return; + } + if (Program.Canceled) return; + foreach (Task task in dlcTasks) + { + if (Program.Canceled) return; + await task; + } + + selection ??= new(); + if (allCheckBox.Checked) selection.Enabled = true; + selection.Usable = true; + selection.Id = appId; + selection.Name = name; + selection.RootDirectory = directory; + selection.DllDirectories = dllDirectories; + selection.IsSteam = true; + selection.AppInfo = appInfo; + selection.IconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString(); + selection.ClientIconStaticID = appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString(); + + if (Program.Canceled) return; + Program.Invoke(selectionTreeView, delegate + { + if (Program.Canceled) return; + TreeNode programNode = TreeNodes.Find(s => s.Name == appId) ?? new(); + programNode.Name = appId; + programNode.Text = name; + programNode.Checked = selection.Enabled; + programNode.Remove(); + selectionTreeView.Nodes.Add(programNode); + foreach (KeyValuePair pair in dlc) { if (Program.Canceled || programNode is null) return; - int appId = pair.Key; + string appId = pair.Key; (string name, string iconStaticId) dlcApp = pair.Value; - selection.AllSteamDlc[appId] = dlcApp; - if (allCheckBox.Checked) selection.SelectedSteamDlc[appId] = dlcApp; - TreeNode dlcNode = TreeNodes.Find(s => s.Name == "" + appId) ?? new(); - dlcNode.Name = appId.ToString(); + selection.AllDlc[appId] = dlcApp; + if (allCheckBox.Checked) selection.SelectedDlc[appId] = dlcApp; + TreeNode dlcNode = TreeNodes.Find(s => s.Name == appId) ?? new(); + dlcNode.Name = appId; dlcNode.Text = dlcApp.name; - dlcNode.Checked = selection.SelectedSteamDlc.ContainsKey(appId); + dlcNode.Checked = selection.SelectedDlc.ContainsKey(appId); dlcNode.Remove(); programNode.Nodes.Add(dlcNode); } - } + }); + if (Program.Canceled) return; + RemoveFromRemainingGames(name); + progress.Report(++CompleteTasks); }); + appTasks.Add(task); + RunningTasks.Add(task); + progress.Report(-RunningTasks.Count); + } + } + if (Directory.Exists(EpicLibrary.EpicAppDataPath)) + { + List epicGames = await EpicLibrary.GetGames(); + Dictionary> games = new(); + foreach (Manifest manifest in epicGames) + { + string id = manifest.CatalogNamespace; + string name = manifest.DisplayName; + string directory = manifest.InstallLocation; + ProgramSelection selection = ProgramSelection.FromId(id); if (Program.Canceled) return; - RemoveFromRemainingGames(name); - progress.Report(++CompleteTasks); - }); - appTasks.Add(task); - RunningTasks.Add(task); - progress.Report(-RunningTasks.Count); + if (Program.IsGameBlocked(name, directory)) continue; + AddToRemainingGames(name); + Task task = Task.Run(async () => + { + if (Program.Canceled) return; + List dllDirectories = await EpicLibrary.GetDllDirectoriesFromGameDirectory(directory); + if (dllDirectories is null) + { + RemoveFromRemainingGames(name); + return; + } + if (Program.Canceled) return; + ConcurrentDictionary dlc = new(); + List dlcTasks = new(); + List<(string id, string name)> dlcIds = await EpicStore.ParseDlcAppIds(id); + if (dlcIds.Count > 0) + { + foreach ((string id, string name) in dlcIds) + { + if (Program.Canceled) return; + AddToRemainingDLCs(id); + Task task = Task.Run(() => + { + if (Program.Canceled) return; + dlc[id] = name; + RemoveFromRemainingDLCs(id); + progress.Report(++CompleteTasks); + }); + dlcTasks.Add(task); + RunningTasks.Add(task); + progress.Report(-RunningTasks.Count); + Thread.Sleep(10); // to reduce control & window freezing + } + } + else + { + RemoveFromRemainingGames(name); + return; + } + if (Program.Canceled) return; + foreach (Task task in dlcTasks) + { + if (Program.Canceled) return; + await task; + } + + selection ??= new(); + if (allCheckBox.Checked) selection.Enabled = true; + selection.Usable = true; + selection.Id = id; + selection.Name = name; + selection.RootDirectory = directory; + selection.DllDirectories = dllDirectories; + + if (Program.Canceled) return; + Program.Invoke(selectionTreeView, delegate + { + if (Program.Canceled) return; + TreeNode programNode = TreeNodes.Find(s => s.Name == id) ?? new(); + programNode.Name = id; + programNode.Text = name; + programNode.Checked = selection.Enabled; + programNode.Remove(); + selectionTreeView.Nodes.Add(programNode); + foreach (KeyValuePair pair in dlc) + { + if (Program.Canceled || programNode is null) return; + string dlcId = pair.Key; + string dlcName = pair.Value; + (string name, string iconStaticId) dlcApp = (dlcName, null); // temporary? + selection.AllDlc[dlcId] = dlcApp; + if (allCheckBox.Checked) selection.SelectedDlc[dlcId] = dlcApp; + TreeNode dlcNode = TreeNodes.Find(s => s.Name == dlcId) ?? new(); + dlcNode.Name = dlcId; + dlcNode.Text = dlcName; + dlcNode.Checked = selection.SelectedDlc.ContainsKey(dlcId); + dlcNode.Remove(); + programNode.Nodes.Add(dlcNode); + } + }); + if (Program.Canceled) return; + RemoveFromRemainingGames(name); + progress.Report(++CompleteTasks); + }); + appTasks.Add(task); + RunningTasks.Add(task); + progress.Report(-RunningTasks.Count); + } } foreach (Task task in appTasks) { @@ -267,17 +374,19 @@ internal partial class SelectForm : CustomForm progressBar.Value = p; }; - progressLabel.Text = $"Setting up SteamCMD . . . "; - await SteamCMD.Setup(iProgress); - + if (Directory.Exists(SteamLibrary.InstallPath)) + { + progressLabel.Text = $"Setting up SteamCMD . . . "; + await SteamCMD.Setup(iProgress); + } 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(); + if (!int.TryParse(node.Name, out int appId) || node.Parent is null && ProgramSelection.FromId(node.Name) is null) node.Remove(); }); - await GetCreamApiApplicablePrograms(iProgress); + await GetApplicablePrograms(iProgress); await SteamCMD.Cleanup(); HideProgressBar(); @@ -302,21 +411,23 @@ internal partial class SelectForm : CustomForm { if (e.Action == TreeViewAction.Unknown) return; TreeNode node = e.Node; - if (node is not null && int.TryParse(node.Name, out int appId)) + if (node is not null) { - ProgramSelection selection = ProgramSelection.FromAppId(appId); + string appId = node.Name; + ProgramSelection selection = ProgramSelection.FromId(appId); if (selection is null) { TreeNode parent = node.Parent; - if (parent is not null && int.TryParse(parent.Name, out int gameAppId)) + if (parent is not null) { - ProgramSelection.FromAppId(gameAppId).ToggleDlc(appId, node.Checked); + string gameAppId = parent.Name; + ProgramSelection.FromId(gameAppId).ToggleDlc(appId, node.Checked); parent.Checked = parent.Nodes.Cast().ToList().Any(treeNode => treeNode.Checked); } } else { - if (selection.AllSteamDlc.Any()) + if (selection.AllDlc.Any()) { selection.ToggleAllDlc(node.Checked); node.Nodes.Cast().ToList().ForEach(treeNode => treeNode.Checked = node.Checked); @@ -345,10 +456,19 @@ internal partial class SelectForm : CustomForm private class TreeNodeSorter : IComparer { - public int Compare(object a, object b) => - !int.TryParse((a as TreeNode).Name, out int A) ? 1 - : !int.TryParse((b as TreeNode).Name, out int B) ? 0 - : A > B ? 1 : 0; + public int Compare(object a, object b) + { + string aId = (a as TreeNode).Name; + string bId = (b as TreeNode).Name; + return aId == "ParadoxLauncher" ? -1 + : bId == "ParadoxLauncher" ? 1 + : !int.TryParse(aId, out _) && !int.TryParse(bId, out _) ? string.Compare(aId, bId) + : !int.TryParse(aId, out int A) ? 1 + : !int.TryParse(bId, out int B) ? -1 + : A > B ? 1 + : A < B ? -1 + : 0; + } } private void ShowProgressBar() @@ -389,11 +509,11 @@ internal partial class SelectForm : CustomForm Dictionary images = new(); Task.Run(async () => { - if (Directory.Exists(SteamLibrary.ParadoxLauncherInstallPath)) + if (Directory.Exists(ParadoxLauncher.InstallPath)) { - foreach (string file in Directory.GetFiles(SteamLibrary.ParadoxLauncherInstallPath, "*.exe")) + foreach (string file in Directory.GetFiles(ParadoxLauncher.InstallPath, "*.exe")) { - images["Paradox Launcher"] = IconGrabber.GetFileIconImage(file); + images["Icon_ParadoxLauncher"] = IconGrabber.GetFileIconImage(file); break; } } @@ -405,7 +525,7 @@ internal partial class SelectForm : CustomForm images["Steam Community"] = await HttpClientManager.GetImageFromUrl("https://steamcommunity.com/favicon.ico"); }); Image Image(string identifier) => images.GetValueOrDefault(identifier, null); - void TrySetImageAsync(ToolStripMenuItem menuItem, int appId, string iconStaticId, bool client = false) => + void TrySetImageAsync(ToolStripMenuItem menuItem, string appId, string iconStaticId, bool client = false) => Task.Run(async () => { menuItem.Image = client ? await IconGrabber.GetSteamClientIcon(appId, iconStaticId) : await IconGrabber.GetSteamIcon(appId, iconStaticId); @@ -415,33 +535,33 @@ internal partial class SelectForm : CustomForm { TreeNode node = e.Node; TreeNode parentNode = node.Parent; - if (!int.TryParse(node.Name, out int appId)) return; - ProgramSelection selection = ProgramSelection.FromAppId(appId); - (int gameAppId, (string name, string iconStaticId) app)? dlc = null; - if (selection is null) dlc = ProgramSelection.GetDlcFromAppId(appId); + string id = node.Name; + ProgramSelection selection = ProgramSelection.FromId(id); + (string gameAppId, (string name, string iconStaticId) app)? dlc = null; + if (selection is null) dlc = ProgramSelection.GetDlcFromId(id); if (e.Button == MouseButtons.Right && node.Bounds.Contains(e.Location)) { selectionTreeView.SelectedNode = node; nodeContextMenu.Items.Clear(); - ToolStripMenuItem header = new(selection?.Name ?? node.Text, Image(appId == 0 ? "Paradox Launcher" : "Icon_" + node.Name)); + ToolStripMenuItem header = new(selection?.Name ?? node.Text, Image("Icon_" + id)); if (header.Image is null) { string iconStaticId = dlc?.app.iconStaticId ?? selection?.IconStaticID; if (iconStaticId is not null) - TrySetImageAsync(header, appId, iconStaticId); + TrySetImageAsync(header, id, iconStaticId); else if (dlc is not null) { - int gameAppId = dlc.Value.gameAppId; + string gameAppId = dlc.Value.gameAppId; header.Image = Image("Icon_" + gameAppId); - ProgramSelection gameSelection = ProgramSelection.FromAppId(gameAppId); + ProgramSelection gameSelection = ProgramSelection.FromId(gameAppId); iconStaticId = gameSelection?.IconStaticID; if (header.Image is null && iconStaticId is not null) TrySetImageAsync(header, gameAppId, iconStaticId); } } nodeContextMenu.Items.Add(header); - string appInfo = $@"{SteamCMD.AppInfoPath}\{appId}.vdf"; - if (appId != 0 && Directory.Exists(Directory.GetDirectoryRoot(appInfo)) && File.Exists(appInfo)) + string appInfo = $@"{SteamCMD.AppInfoPath}\{id}.vdf"; + if (Directory.Exists(Directory.GetDirectoryRoot(appInfo)) && File.Exists(appInfo)) { nodeContextMenu.Items.Add(new ToolStripSeparator()); nodeContextMenu.Items.Add(new ToolStripMenuItem("Open AppInfo", Image("Notepad"), @@ -459,39 +579,54 @@ internal partial class SelectForm : CustomForm } if (selection is not null) { - if (appId == 0) + if (id == "ParadoxLauncher") { nodeContextMenu.Items.Add(new ToolStripSeparator()); nodeContextMenu.Items.Add(new ToolStripMenuItem("Repair", Image("Command Prompt"), new EventHandler(async (sender, e) => { if (!Program.IsProgramRunningDialog(this, selection)) return; + byte[] cApiIni = null; byte[] properApi = null; byte[] properApi64 = null; - foreach (string directory in selection.SteamApiDllDirectories) + + byte[] sApiJson = null; + byte[] properSdk = null; + byte[] properSdk64 = null; + + foreach (string directory in selection.DllDirectories) { - directory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + directory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); if (cApiIni is null && File.Exists(cApi)) cApiIni = File.ReadAllBytes(cApi); await InstallForm.UninstallCreamAPI(directory); - if (properApi is null && File.Exists(api) && !FileResourceExtensions.Equals(Properties.Resources.API, api)) + if (properApi is null && File.Exists(api) && !Properties.Resources.API.EqualsFile(api)) properApi = File.ReadAllBytes(api); - if (properApi64 is null && File.Exists(api64) && !FileResourceExtensions.Equals(Properties.Resources.API64, api64)) + if (properApi64 is null && File.Exists(api64) && !Properties.Resources.API64.EqualsFile(api64)) properApi64 = File.ReadAllBytes(api64); + + directory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (sApiJson is null && File.Exists(sApi)) + sApiJson = File.ReadAllBytes(sApi); + await InstallForm.UninstallCreamAPI(directory); + if (properSdk is null && File.Exists(sdk) && !Properties.Resources.SDK.EqualsFile(sdk)) + properSdk = File.ReadAllBytes(sdk); + if (properSdk64 is null && File.Exists(sdk64) && !Properties.Resources.SDK64.EqualsFile(sdk64)) + properSdk64 = File.ReadAllBytes(sdk64); } - if (properApi is not null || properApi64 is not null) + if (properApi is not null || properApi64 is not null || properSdk is not null || properSdk64 is not null) { bool neededRepair = false; - foreach (string directory in selection.SteamApiDllDirectories) + foreach (string directory in selection.DllDirectories) { - directory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); - if (properApi is not null && FileResourceExtensions.Equals(Properties.Resources.API, api)) + directory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + if (properApi is not null && Properties.Resources.API.EqualsFile(api)) { properApi.Write(api); neededRepair = true; } - if (properApi64 is not null && FileResourceExtensions.Equals(Properties.Resources.API64, api64)) + if (properApi64 is not null && Properties.Resources.API64.EqualsFile(api64)) { properApi64.Write(api64); neededRepair = true; @@ -501,46 +636,67 @@ internal partial class SelectForm : CustomForm await InstallForm.InstallCreamAPI(directory, selection); cApiIni.Write(cApi); } + + directory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (properSdk is not null && Properties.Resources.SDK.EqualsFile(sdk)) + { + properSdk.Write(sdk); + neededRepair = true; + } + if (properSdk64 is not null && Properties.Resources.SDK64.EqualsFile(sdk64)) + { + properSdk64.Write(sdk64); + neededRepair = true; + } + if (sApiJson is not null) + { + await InstallForm.InstallScreamAPI(directory, selection); + sApiJson.Write(sApi); + } } if (neededRepair) - new DialogForm(this).Show("Paradox Launcher Repair", Icon, "Paradox Launcher successfully repaired!", "OK"); + new DialogForm(this).Show(Icon, "Paradox Launcher successfully repaired!", "OK"); else - new DialogForm(this).Show("Paradox Launcher Repair", SystemIcons.Information, "Paradox Launcher does not need to be repaired.", "OK"); + new DialogForm(this).Show(SystemIcons.Information, "Paradox Launcher does not need to be repaired.", "OK"); } else - new DialogForm(this).Show("Paradox Launcher Repair", SystemIcons.Error, "Paradox Launcher repair failed!" - + "\n\nAn original Steamworks API file could not be found." + new DialogForm(this).Show(SystemIcons.Error, "Paradox Launcher repair failed!" + + "\n\nAn original Steamworks API or EOS SDK file could not be found." + "\nYou must reinstall Paradox Launcher to fix this issue.", "OK"); }))); } nodeContextMenu.Items.Add(new ToolStripSeparator()); nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Root Directory", Image("File Explorer"), new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(selection.RootDirectory)))); - for (int i = 0; i < selection.SteamApiDllDirectories.Count; i++) + for (int i = 0; i < selection.DllDirectories.Count; i++) { - string directory = selection.SteamApiDllDirectories[i]; - nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open Steamworks Directory ({i + 1})", Image("File Explorer"), + string directory = selection.DllDirectories[i]; + nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open {(selection.IsSteam ? "Steamworks API" : "EOS SDK")} Directory ({i + 1})", Image("File Explorer"), new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(directory)))); } } - if (appId != 0) + if (id != "ParadoxLauncher" && selection is not null) { - nodeContextMenu.Items.Add(new ToolStripSeparator()); - nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"), - new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamdb.info/app/" + appId)))); - nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Store", Image("Steam Store"), - new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://store.steampowered.com/app/" + appId)))); - if (selection is not null) + if (selection.IsSteam) { - ToolStripMenuItem steamCommunity = new("Open Steam Community", Image("ClientIcon_" + node.Name), - new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamcommunity.com/app/" + appId))); + nodeContextMenu.Items.Add(new ToolStripSeparator()); + nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"), + new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamdb.info/app/" + id)))); + nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Store", Image("Steam Store"), + new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://store.steampowered.com/app/" + id)))); + ToolStripMenuItem steamCommunity = new("Open Steam Community", Image("ClientIcon_" + id), + new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamcommunity.com/app/" + id))); nodeContextMenu.Items.Add(steamCommunity); if (steamCommunity.Image is null) { steamCommunity.Image = Image("Steam Community"); - TrySetImageAsync(steamCommunity, appId, selection.ClientIconStaticID, true); + TrySetImageAsync(steamCommunity, id, selection.ClientIconStaticID, true); } } + else + { + // Epic Games links? + } } nodeContextMenu.Show(selectionTreeView, e.Location); } @@ -548,52 +704,13 @@ internal partial class SelectForm : CustomForm OnLoad(); } - private static void PopulateParadoxLauncherDlc(ProgramSelection paradoxLauncher = null) - { - paradoxLauncher ??= ProgramSelection.FromAppId(0); - if (paradoxLauncher is not null) - { - 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?.GetChild("extended")?.GetChild("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; + if (ParadoxLauncher.DlcDialog(this)) return; Hide(); InstallForm installForm = new(this, uninstall); installForm.ShowDialog(); @@ -655,9 +772,9 @@ internal partial class SelectForm : CustomForm string blockedDirectoryExceptions = ""; foreach (string name in Program.ProtectedGameDirectoryExceptions) blockedDirectoryExceptions += helpButtonListPrefix + name; - new DialogForm(this).Show(blockedGamesCheckBox.Text, SystemIcons.Information, + new DialogForm(this).Show(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." + + "\nanti-cheats, or that are confirmed not to be working with CreamAPI or ScreamAPI." + "\n\nBlocked game names:" + blockedGames + "\n\nBlocked game sub-directories:" + blockedDirectories + "\n\nBlocked game sub-directory exceptions (not blocked):" + blockedDirectoryExceptions, diff --git a/CreamInstaller/Paradox/ParadoxLauncher.cs b/CreamInstaller/Paradox/ParadoxLauncher.cs new file mode 100644 index 0000000..0471e13 --- /dev/null +++ b/CreamInstaller/Paradox/ParadoxLauncher.cs @@ -0,0 +1,62 @@ +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +using CreamInstaller.Steam; + +using Microsoft.Win32; + +namespace CreamInstaller.Paradox; + +internal static class ParadoxLauncher +{ + private static string installPath = null; + internal static string InstallPath + { + get + { + installPath ??= Registry.GetValue(@"HKEY_CURRENT_USER\Software\Paradox Interactive\Paradox Launcher v2", "LauncherInstallation", null) as string; + installPath ??= Registry.GetValue(@"HKEY_CURRENT_USER\Software\Wow6432Node\Paradox Interactive\Paradox Launcher v2", "LauncherInstallation", null) as string; + return installPath; + } + } + + private static void PopulateDlc(ProgramSelection paradoxLauncher = null) + { + paradoxLauncher ??= ProgramSelection.FromId("ParadoxLauncher"); + if (paradoxLauncher is not null) + { + paradoxLauncher.ExtraDlc.Clear(); + foreach (ProgramSelection selection in ProgramSelection.AllUsableEnabled) + { + if (selection == paradoxLauncher) continue; + if (selection.AppInfo is null || selection.AppInfo.Value?.GetChild("extended")?.GetChild("publisher")?.ToString() != "Paradox Interactive") continue; + paradoxLauncher.ExtraDlc.Add(new(selection.Id, selection.Name, selection.SelectedDlc)); + } + if (!paradoxLauncher.ExtraDlc.Any()) + foreach (ProgramSelection selection in ProgramSelection.AllUsable) + { + if (selection == paradoxLauncher) continue; + if (selection.AppInfo is null || selection.AppInfo.Value?.GetChild("extended")?.GetChild("publisher")?.ToString() != "Paradox Interactive") continue; + paradoxLauncher.ExtraDlc.Add(new(selection.Id, selection.Name, selection.AllDlc)); + } + } + } + + internal static bool DlcDialog(Form form) + { + ProgramSelection paradoxLauncher = ProgramSelection.FromId("ParadoxLauncher"); + if (paradoxLauncher is not null && paradoxLauncher.Enabled) + { + PopulateDlc(paradoxLauncher); + if (!paradoxLauncher.ExtraDlc.Any()) + { + return new DialogForm(form).Show(SystemIcons.Warning, + $"WARNING: There are no installed games with DLC that can be added to the Paradox Launcher!" + + "\n\nInstalling CreamAPI/ScreamAPI for the Paradox Launcher is pointless, since no DLC will be added to the configuration!", + "Ignore", "Cancel") != DialogResult.OK; + } + } + return false; + } +} diff --git a/CreamInstaller/Program.cs b/CreamInstaller/Program.cs index 8847f29..17a9648 100644 --- a/CreamInstaller/Program.cs +++ b/CreamInstaller/Program.cs @@ -7,20 +7,22 @@ using System.Reflection; using System.Threading; using System.Windows.Forms; -using CreamInstaller.Classes; +using CreamInstaller.Steam; +using CreamInstaller.Utility; namespace CreamInstaller; internal static class Program { internal static readonly string ApplicationName = Application.CompanyName + " v" + Application.ProductVersion + ": " + Application.ProductName; + internal static readonly string ApplicationNameShort = Application.CompanyName + " v" + Application.ProductVersion; internal static readonly Assembly EntryAssembly = Assembly.GetEntryAssembly(); internal static readonly Process CurrentProcess = Process.GetCurrentProcess(); internal static readonly string CurrentProcessFilePath = CurrentProcess.MainModule.FileName; 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[] ProtectedGameNames = { "PAYDAY 2", "Call to Arms" }; // non-functioning CreamAPI/ScreamAPI 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? @@ -44,9 +46,9 @@ internal static class Program internal static bool IsProgramRunningDialog(Form form, ProgramSelection selection) { - if (selection.AreSteamApiDllsLocked) + if (selection.AreDllsLocked) { - if (new DialogForm(form).Show(ApplicationName, SystemIcons.Error, + if (new DialogForm(form).Show(SystemIcons.Error, $"ERROR: {selection.Name} is currently running!" + "\n\nPlease close the program/game to continue . . . ", "Retry", "Cancel") == DialogResult.OK) @@ -56,7 +58,7 @@ internal static class Program return false; } - internal static void GetApiComponents(this string directory, out string api, out string api_o, out string api64, out string api64_o, out string cApi) + internal static void GetCreamApiComponents(this string directory, out string api, out string api_o, out string api64, out string api64_o, out string cApi) { api = directory + @"\steam_api.dll"; api_o = directory + @"\steam_api_o.dll"; @@ -65,6 +67,15 @@ internal static class Program cApi = directory + @"\cream_api.ini"; } + internal static void GetScreamApiComponents(this string directory, out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi) + { + sdk = directory + @"\EOSSDK-Win32-Shipping.dll"; + sdk_o = directory + @"\EOSSDK-Win32-Shipping_o.dll"; + sdk64 = directory + @"\EOSSDK-Win64-Shipping.dll"; + sdk64_o = directory + @"\EOSSDK-Win64-Shipping_o.dll"; + sApi = directory + @"\ScreamAPI.json"; + } + [STAThread] private static void Main() { diff --git a/CreamInstaller/ProgramSelection.cs b/CreamInstaller/ProgramSelection.cs new file mode 100644 index 0000000..f530e7b --- /dev/null +++ b/CreamInstaller/ProgramSelection.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Gameloop.Vdf.Linq; + +namespace CreamInstaller; + +internal class ProgramSelection +{ + internal bool Enabled = false; + internal bool Usable = true; + + internal string Id = "0"; + internal string Name = "Program"; + + internal string IconStaticID = null; + internal string ClientIconStaticID = null; + + internal string RootDirectory = null; + internal List DllDirectories = null; + + internal bool IsSteam = false; + internal VProperty AppInfo = null; + + internal readonly SortedList AllDlc = new(); + internal readonly SortedList SelectedDlc = new(); + internal readonly List>> ExtraDlc = new(); + + internal bool AreDllsLocked + { + get + { + foreach (string directory in DllDirectories) + { + directory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + if (api.IsFilePathLocked() + || api_o.IsFilePathLocked() + || api64.IsFilePathLocked() + || api64_o.IsFilePathLocked() + || cApi.IsFilePathLocked()) + return true; + directory.GetScreamApiComponents(out string sdk, out string sdk_o, out string sdk64, out string sdk64_o, out string sApi); + if (sdk.IsFilePathLocked() + || sdk_o.IsFilePathLocked() + || sdk64.IsFilePathLocked() + || sdk64_o.IsFilePathLocked() + || sApi.IsFilePathLocked()) + return true; + } + return false; + } + } + + private void Toggle(string dlcAppId, (string name, string iconStaticId) dlcApp, bool enabled) + { + if (enabled) SelectedDlc[dlcAppId] = dlcApp; + else SelectedDlc.Remove(dlcAppId); + } + + internal void ToggleDlc(string dlcAppId, bool enabled) + { + foreach (KeyValuePair pair in AllDlc) + { + string appId = pair.Key; + (string name, string iconStaticId) dlcApp = pair.Value; + if (appId == dlcAppId) + { + Toggle(appId, dlcApp, enabled); + break; + } + } + Enabled = SelectedDlc.Any(); + } + + internal void ToggleAllDlc(bool enabled) + { + if (!enabled) SelectedDlc.Clear(); + else foreach (KeyValuePair pair in AllDlc) + { + string appId = pair.Key; + (string name, string iconStaticId) dlcApp = pair.Value; + Toggle(appId, dlcApp, enabled); + } + Enabled = SelectedDlc.Any(); + } + + internal ProgramSelection() => All.Add(this); + + internal void Validate() + { + if (Program.IsGameBlocked(Name, RootDirectory)) + { + All.Remove(this); + return; + } + if (!Directory.Exists(RootDirectory)) + { + All.Remove(this); + return; + } + DllDirectories.RemoveAll(directory => !Directory.Exists(directory)); + if (!DllDirectories.Any()) All.Remove(this); + } + + internal static void ValidateAll() => AllSafe.ForEach(selection => selection.Validate()); + + internal static List All = new(); + + 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 FromId(string id) => AllUsable.Find(s => s.Id == id); + + internal static (string gameAppId, (string name, string iconStaticId) app)? GetDlcFromId(string appId) + { + foreach (ProgramSelection selection in AllUsable) + foreach (KeyValuePair pair in selection.AllDlc) + if (pair.Key == appId) return (selection.Id, pair.Value); + return null; + } +} diff --git a/CreamInstaller/Properties/Resources.Designer.cs b/CreamInstaller/Properties/Resources.Designer.cs index 6848b20..fc09aec 100644 --- a/CreamInstaller/Properties/Resources.Designer.cs +++ b/CreamInstaller/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace CreamInstaller.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -89,5 +89,25 @@ namespace CreamInstaller.Properties { return ((System.Drawing.Icon)(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SDK { + get { + object obj = ResourceManager.GetObject("SDK", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SDK64 { + get { + object obj = ResourceManager.GetObject("SDK64", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/CreamInstaller/Properties/Resources.resx b/CreamInstaller/Properties/Resources.resx index 735ca66..76a8f78 100644 --- a/CreamInstaller/Properties/Resources.resx +++ b/CreamInstaller/Properties/Resources.resx @@ -112,12 +112,12 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + ..\resources\steam_api.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -127,4 +127,10 @@ ..\resources\ini.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\EOSSDK-Win32-Shipping.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\EOSSDK-Win64-Shipping.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/CreamInstaller/Resources/EOSSDK-Win32-Shipping.dll b/CreamInstaller/Resources/EOSSDK-Win32-Shipping.dll new file mode 100644 index 0000000..935929f Binary files /dev/null and b/CreamInstaller/Resources/EOSSDK-Win32-Shipping.dll differ diff --git a/CreamInstaller/Resources/EOSSDK-Win64-Shipping.dll b/CreamInstaller/Resources/EOSSDK-Win64-Shipping.dll new file mode 100644 index 0000000..527d515 Binary files /dev/null and b/CreamInstaller/Resources/EOSSDK-Win64-Shipping.dll differ diff --git a/CreamInstaller/Resources/FileResourceExtensions.cs b/CreamInstaller/Resources/FileResourceExtensions.cs index 7d03ece..bbfaffe 100644 --- a/CreamInstaller/Resources/FileResourceExtensions.cs +++ b/CreamInstaller/Resources/FileResourceExtensions.cs @@ -10,7 +10,7 @@ internal static class FileResourceExtensions file.Write(resource); } - internal static bool Equals(byte[] resource, string filePath) + internal static bool EqualsFile(this byte[] resource, string filePath) { byte[] file = File.ReadAllBytes(filePath); if (resource.Length != file.Length) diff --git a/CreamInstaller/Classes/SteamCMD.cs b/CreamInstaller/Steam/SteamCMD.cs similarity index 93% rename from CreamInstaller/Classes/SteamCMD.cs rename to CreamInstaller/Steam/SteamCMD.cs index 75376f0..8f0e427 100644 --- a/CreamInstaller/Classes/SteamCMD.cs +++ b/CreamInstaller/Steam/SteamCMD.cs @@ -14,7 +14,7 @@ using CreamInstaller.Resources; using Gameloop.Vdf.Linq; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Steam; internal static class SteamCMD { @@ -120,6 +120,7 @@ internal static class SteamCMD internal static async Task Cleanup() => await Task.Run(async () => { + if (!Directory.Exists(DirectoryPath)) return; await Kill(); try { @@ -171,7 +172,7 @@ internal static class SteamCMD catch { } }); - internal static async Task GetAppInfo(int appId, string branch = "public", int buildId = 0) + internal static async Task GetAppInfo(string appId, string branch = "public", int buildId = 0) { if (Program.Canceled) return null; string output; @@ -200,10 +201,8 @@ internal static class SteamCMD goto restart; } if (appInfo.Value is VValue) - { //new DialogForm(null).Show("GetAppInfo", SystemIcons.Information, "VValue exception:\n\n" + output, "OK"); 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") @@ -212,8 +211,8 @@ internal static class SteamCMD if (buildid is null && type is not null) return appInfo; if (type is null || int.TryParse(buildid, out int gamebuildId) && gamebuildId < buildId) { - List dlcAppIds = await ParseDlcAppIds(appInfo); - foreach (int id in dlcAppIds) + List dlcAppIds = await ParseDlcAppIds(appInfo); + foreach (string id in dlcAppIds) { string dlcAppUpdateFile = $@"{AppInfoPath}\{id}.vdf"; if (File.Exists(dlcAppUpdateFile)) File.Delete(dlcAppUpdateFile); @@ -225,9 +224,9 @@ internal static class SteamCMD return appInfo; } - internal static async Task> ParseDlcAppIds(VProperty appInfo) => await Task.Run(() => + internal static async Task> ParseDlcAppIds(VProperty appInfo) => await Task.Run(() => { - List dlcIds = new(); + List dlcIds = new(); #pragma warning disable IDE0150 // Prefer 'null' check over type check if (Program.Canceled || appInfo is not VProperty) return dlcIds; #pragma warning restore IDE0150 // Prefer 'null' check over type check @@ -236,13 +235,15 @@ internal static class SteamCMD foreach (VProperty property in extended) if (property.Key == "listofdlc") foreach (string id in property.Value.ToString().Split(",")) - if (int.TryParse(id, out int appId) && !dlcIds.Contains(appId)) dlcIds.Add(appId); + if (int.TryParse(id, out int appId) + && !dlcIds.Contains("" + appId)) + dlcIds.Add("" + appId); VToken depots = appInfo.Value.GetChild("depots"); if (depots is not null) foreach (VProperty property in depots) if (int.TryParse(property.Key, out int _) && int.TryParse(property.Value.GetChild("dlcappid")?.ToString(), out int appid) - && !dlcIds.Contains(appid)) - dlcIds.Add(appid); + && !dlcIds.Contains("" + appid)) + dlcIds.Add("" + appid); return dlcIds; }); @@ -250,7 +251,6 @@ internal static class SteamCMD { List tasks = new(); foreach (Process process in Process.GetProcessesByName("steamcmd")) - { tasks.Add(Task.Run(() => { try @@ -260,7 +260,6 @@ internal static class SteamCMD } catch { } })); - } foreach (Task task in tasks) await task; } diff --git a/CreamInstaller/Classes/SteamLibrary.cs b/CreamInstaller/Steam/SteamLibrary.cs similarity index 71% rename from CreamInstaller/Classes/SteamLibrary.cs rename to CreamInstaller/Steam/SteamLibrary.cs index 403ef8f..0f1baed 100644 --- a/CreamInstaller/Classes/SteamLibrary.cs +++ b/CreamInstaller/Steam/SteamLibrary.cs @@ -9,62 +9,64 @@ using Gameloop.Vdf.Linq; using Microsoft.Win32; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Steam; internal static class SteamLibrary { - internal static string paradoxLauncherInstallPath = null; - internal static string ParadoxLauncherInstallPath + private static string installPath = null; + internal static string InstallPath { get { - paradoxLauncherInstallPath ??= Registry.GetValue(@"HKEY_CURRENT_USER\Software\Paradox Interactive\Paradox Launcher v2", "LauncherInstallation", null) as string; - return paradoxLauncherInstallPath; + installPath ??= Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Valve\Steam", "InstallPath", null) as string; + installPath ??= Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Valve\Steam", "InstallPath", null) as string; + return installPath; } } - internal static string steamInstallPath = null; - internal static string SteamInstallPath + internal static async Task>> GetGames() => await Task.Run(async () => { - get + List> games = new(); + List gameLibraryDirectories = await GetLibraryDirectories(); + foreach (string libraryDirectory in gameLibraryDirectories) { - 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; - return steamInstallPath; + List> directoryGames = await GetGamesFromLibraryDirectory(libraryDirectory); + if (directoryGames is not null) + foreach (Tuple game in directoryGames) + if (!games.Any(_game => _game.Item1 == game.Item1)) + games.Add(game); } - } - - internal static async Task> GetLibraryDirectories() => await Task.Run(() => - { - List gameDirectories = new(); - if (Program.Canceled) return gameDirectories; - string steamInstallPath = SteamInstallPath; - if (steamInstallPath != null && Directory.Exists(steamInstallPath)) - { - 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)) - { - 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; + return games; }); - internal static async Task>> GetGamesFromLibraryDirectory(string libraryDirectory) => await Task.Run(() => + internal static async Task> GetDllDirectoriesFromGameDirectory(string gameDirectory) => await Task.Run(async () => { - List> games = new(); + List dllDirectories = new(); + if (Program.Canceled || !Directory.Exists(gameDirectory)) return null; + gameDirectory.GetCreamApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); + if (File.Exists(api) + || File.Exists(api_o) + || File.Exists(api64) + || File.Exists(api64_o) + || File.Exists(cApi)) + 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 { } + } + return !dllDirectories.Any() ? null : dllDirectories; + }); + + internal 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) @@ -86,34 +88,35 @@ internal static class SteamLibrary 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)); + games.Add(new(appId, name, branch, buildIdInt, gameDirectory)); } } return !games.Any() ? null : games; }); - internal static async Task> GetDllDirectoriesFromGameDirectory(string gameDirectory) => await Task.Run(async () => + internal static async Task> GetLibraryDirectories() => await Task.Run(() => { - List dllDirectories = new(); - if (Program.Canceled || !Directory.Exists(gameDirectory)) return null; - gameDirectory.GetApiComponents(out string api, out string api_o, out string api64, out string api64_o, out string cApi); - if (File.Exists(api) - || File.Exists(api_o) - || File.Exists(api64) - || File.Exists(api64_o) - || File.Exists(cApi)) - dllDirectories.Add(gameDirectory); - string[] directories = Directory.GetDirectories(gameDirectory); - foreach (string _directory in directories) + List gameDirectories = new(); + if (Program.Canceled) return gameDirectories; + string steamInstallPath = InstallPath; + if (steamInstallPath != null && Directory.Exists(steamInstallPath)) { - if (Program.Canceled) return null; - try + string libraryFolder = steamInstallPath + @"\steamapps"; + if (Directory.Exists(libraryFolder)) { - List moreDllDirectories = await GetDllDirectoriesFromGameDirectory(_directory); - if (moreDllDirectories is not null) dllDirectories.AddRange(moreDllDirectories); + gameDirectories.Add(libraryFolder); + string libraryFolders = libraryFolder + @"\libraryfolders.vdf"; + if (File.Exists(libraryFolders) && ValveDataFile.TryDeserialize(File.ReadAllText(libraryFolders, Encoding.UTF8), out VProperty result)) + 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); + } } - catch { } } - return !dllDirectories.Any() ? null : dllDirectories; + return gameDirectories; }); } diff --git a/CreamInstaller/Steam/SteamStore.cs b/CreamInstaller/Steam/SteamStore.cs new file mode 100644 index 0000000..06acaff --- /dev/null +++ b/CreamInstaller/Steam/SteamStore.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using CreamInstaller.Utility; + +using HtmlAgilityPack; + +namespace CreamInstaller.Steam; + +internal static class SteamStore +{ + internal static async Task ParseDlcAppIds(string appId, List dlcIds) + { + // currently this is only really needed to get DLC that release without changing game buildid (very rare) + // it also finds things which aren't really connected to the game itself, and thus not needed (usually soundtracks, collections, packs, etc.) + HtmlNodeCollection nodes = await HttpClientManager.GetDocumentNodes( + $"https://store.steampowered.com/dlc/{appId}", + "//div[@class='recommendation']/div/a"); + if (nodes is not null) + foreach (HtmlNode node in nodes) + if (int.TryParse(node.Attributes?["data-ds-appid"]?.Value, out int dlcAppId) && dlcAppId > 0 && !dlcIds.Contains("" + dlcAppId)) + dlcIds.Add("" + dlcAppId); + } +} diff --git a/CreamInstaller/Classes/ValveDataFile.cs b/CreamInstaller/Steam/ValveDataFile.cs similarity index 94% rename from CreamInstaller/Classes/ValveDataFile.cs rename to CreamInstaller/Steam/ValveDataFile.cs index b32027f..f6c1bdc 100644 --- a/CreamInstaller/Classes/ValveDataFile.cs +++ b/CreamInstaller/Steam/ValveDataFile.cs @@ -2,7 +2,7 @@ using Gameloop.Vdf; using Gameloop.Vdf.Linq; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Steam; internal static class ValveDataFile { diff --git a/CreamInstaller/Classes/Diagnostics.cs b/CreamInstaller/Utility/Diagnostics.cs similarity index 94% rename from CreamInstaller/Classes/Diagnostics.cs rename to CreamInstaller/Utility/Diagnostics.cs index 895023e..0f03d77 100644 --- a/CreamInstaller/Classes/Diagnostics.cs +++ b/CreamInstaller/Utility/Diagnostics.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Utility; internal static class Diagnostics { diff --git a/CreamInstaller/Classes/ExceptionHandler.cs b/CreamInstaller/Utility/ExceptionHandler.cs similarity index 97% rename from CreamInstaller/Classes/ExceptionHandler.cs rename to CreamInstaller/Utility/ExceptionHandler.cs index d208747..b30ddfd 100644 --- a/CreamInstaller/Classes/ExceptionHandler.cs +++ b/CreamInstaller/Utility/ExceptionHandler.cs @@ -1,7 +1,7 @@ using System; using System.Windows.Forms; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Utility; internal static class ExceptionHandler { diff --git a/CreamInstaller/Utility/HttpClientManager.cs b/CreamInstaller/Utility/HttpClientManager.cs new file mode 100644 index 0000000..1f4e3f8 --- /dev/null +++ b/CreamInstaller/Utility/HttpClientManager.cs @@ -0,0 +1,50 @@ +using System; +using System.Drawing; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +using HtmlAgilityPack; + +namespace CreamInstaller.Utility; + +internal static class HttpClientManager +{ + internal static HttpClient HttpClient; + internal static void Setup() + { + HttpClient = new(); + HttpClient.DefaultRequestHeaders.Add("user-agent", $"CreamInstaller-{Environment.MachineName}_{Environment.UserDomainName}_{Environment.UserName}"); + } + + internal static async Task Get(string url) + { + try + { + 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); + HtmlDocument document = new(); + document.LoadHtml(reader.ReadToEnd()); + return document; + } + catch { return null; } + } + + internal static async Task GetDocumentNodes(string url, string xpath) => (await Get(url))?.DocumentNode?.SelectNodes(xpath); + + internal static async Task GetImageFromUrl(string url) + { + try + { + return new Bitmap(await HttpClient.GetStreamAsync(url)); + } + catch { } + return null; + } + + internal static void Dispose() => HttpClient.Dispose(); +} diff --git a/CreamInstaller/Classes/IconGrabber.cs b/CreamInstaller/Utility/IconGrabber.cs similarity index 69% rename from CreamInstaller/Classes/IconGrabber.cs rename to CreamInstaller/Utility/IconGrabber.cs index 6ee2bf2..a5be27c 100644 --- a/CreamInstaller/Classes/IconGrabber.cs +++ b/CreamInstaller/Utility/IconGrabber.cs @@ -3,7 +3,7 @@ using System.Drawing; using System.IO; using System.Threading.Tasks; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Utility; internal static class IconGrabber { @@ -14,8 +14,8 @@ internal static class IconGrabber } private static readonly string SteamAppImagesPath = "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/"; - internal static async Task GetSteamIcon(int steamAppId, string iconStaticId) => await HttpClientManager.GetImageFromUrl(SteamAppImagesPath + $"/{steamAppId}/{iconStaticId}.jpg"); - internal static async Task GetSteamClientIcon(int steamAppId, string clientIconStaticId) => await HttpClientManager.GetImageFromUrl(SteamAppImagesPath + $"/{steamAppId}/{clientIconStaticId}.ico"); + internal static async Task GetSteamIcon(string steamAppId, string iconStaticId) => await HttpClientManager.GetImageFromUrl(SteamAppImagesPath + $"/{steamAppId}/{iconStaticId}.jpg"); + internal static async Task GetSteamClientIcon(string steamAppId, string clientIconStaticId) => await HttpClientManager.GetImageFromUrl(SteamAppImagesPath + $"/{steamAppId}/{clientIconStaticId}.ico"); internal static Image GetFileIconImage(string path) => File.Exists(path) ? Icon.ExtractAssociatedIcon(path).ToBitmap() : null; diff --git a/CreamInstaller/Classes/InstallationLog.cs b/CreamInstaller/Utility/InstallationLog.cs similarity index 96% rename from CreamInstaller/Classes/InstallationLog.cs rename to CreamInstaller/Utility/InstallationLog.cs index 7842f89..20b8eb4 100644 --- a/CreamInstaller/Classes/InstallationLog.cs +++ b/CreamInstaller/Utility/InstallationLog.cs @@ -1,7 +1,7 @@ using System.Drawing; using System.Windows.Forms; -namespace CreamInstaller.Classes; +namespace CreamInstaller.Utility; internal static class InstallationLog {