diff --git a/CreamInstaller/Components/CustomTreeView.cs b/CreamInstaller/Components/CustomTreeView.cs index 74ae1e0..8b510bc 100644 --- a/CreamInstaller/Components/CustomTreeView.cs +++ b/CreamInstaller/Components/CustomTreeView.cs @@ -34,7 +34,8 @@ internal class CustomTreeView : TreeView Font subFont = new(font.FontFamily, font.SizeInPoints, FontStyle.Regular, font.Unit, font.GdiCharSet, font.GdiVerticalFont); string subText = node.Name; - if (string.IsNullOrWhiteSpace(subText) || subText == "ParadoxLauncher" || subText[0] == 'v' && Version.TryParse(subText[1..], out _)) + if (string.IsNullOrWhiteSpace(subText) || subText == "ParadoxLauncher" + || ProgramSelection.FromId(subText) is null && ProgramSelection.GetDlcFromId(subText) is null) return; Size subSize = TextRenderer.MeasureText(graphics, subText, subFont); diff --git a/CreamInstaller/CreamInstaller.csproj b/CreamInstaller/CreamInstaller.csproj index 280cb25..11ffc83 100644 --- a/CreamInstaller/CreamInstaller.csproj +++ b/CreamInstaller/CreamInstaller.csproj @@ -5,7 +5,7 @@ True Resources\ini.ico true - 3.0.1.1 + 3.0.2.0 Resources\ini.ico diff --git a/CreamInstaller/Epic/EpicStore.cs b/CreamInstaller/Epic/EpicStore.cs index c23ee5a..bf24ce8 100644 --- a/CreamInstaller/Epic/EpicStore.cs +++ b/CreamInstaller/Epic/EpicStore.cs @@ -15,18 +15,41 @@ namespace CreamInstaller.Epic; internal static class EpicStore { - internal static async Task> ParseDlcIds(string categoryNamespace) + // need a method to query catalog items + + internal static async Task> QueryEntitlements(Manifest manifest) { - // this method does not yet find ALL dlcs + string @namespace = manifest.CatalogNamespace; + string mainId = manifest.MainGameCatalogItemId; List<(string id, string name, string product, string icon, string developer)> dlcIds = new(); - Response response = await QueryGraphQL(categoryNamespace); + Response response = await QueryGraphQL(@namespace); if (response is null) return dlcIds; - try { File.WriteAllText(ProgramData.AppInfoPath + @$"\{categoryNamespace}.json", JsonConvert.SerializeObject(response, Formatting.Indented)); } catch { } - List elements = new(response.Data.Catalog.CatalogOffers.Elements); - foreach (Element element in elements) + try { File.WriteAllText(ProgramData.AppInfoPath + @$"\{@namespace}.json", JsonConvert.SerializeObject(response, Formatting.Indented)); } catch { } + List storeElements = new(response.Data.Catalog.SearchStore.Elements); + foreach (Element element in storeElements) { - string product = null; - try { product = element.CatalogNs.Mappings[0].PageSlug; } catch { } + string title = element.Title; + string product = (element.CatalogNs is not null && element.CatalogNs.Mappings.Any()) + ? element.CatalogNs.Mappings.First().PageSlug : null; + string icon = null; + for (int i = 0; i < element.KeyImages?.Length; i++) + { + KeyImage keyImage = element.KeyImages[i]; + if (keyImage.Type == "DieselStoreFront") + { + icon = keyImage.Url; + break; + } + } + foreach (Item item in element.Items) + dlcIds.Populate(item.Id, title, product, icon, null, canOverwrite: element.Items.Length == 1); + } + List catalogElements = new(response.Data.Catalog.CatalogOffers.Elements); + foreach (Element element in catalogElements) + { + string title = element.Title; + string product = (element.CatalogNs is not null && element.CatalogNs.Mappings.Any()) + ? element.CatalogNs.Mappings.First().PageSlug : null; string icon = null; for (int i = 0; i < element.KeyImages?.Length; i++) { @@ -38,16 +61,31 @@ internal static class EpicStore } } foreach (Item item in element.Items) - { - (string id, string name, string product, string icon, string developer) app = (item.Id, element.Title, product, icon, item.Developer); - if (!dlcIds.Any(a => a.id == app.id)) - dlcIds.Add(app); - } + dlcIds.Populate(item.Id, title, product, icon, item.Developer, canOverwrite: element.Items.Length == 1); } return dlcIds; } - internal static async Task QueryGraphQL(string categoryNamespace) + private static void Populate(this List<(string id, string name, string product, string icon, string developer)> dlcIds, string id, string title, string product, string icon, string developer, bool canOverwrite = false) + { + if (id == null) return; + bool found = false; + for (int i = 0; i < dlcIds.Count; i++) + { + (string id, string name, string product, string icon, string developer) app = dlcIds[i]; + if (app.id == id) + { + found = true; + dlcIds[i] = canOverwrite + ? (app.id, title ?? app.name, product ?? app.product, icon ?? app.icon, developer ?? app.developer) + : (app.id, app.name ?? title, app.product ?? product, app.icon ?? icon, app.developer ?? developer); + break; + } + } + if (!found) dlcIds.Add((id, title, product, icon, developer)); + } + + private static async Task QueryGraphQL(string categoryNamespace) { string encoded = HttpUtility.UrlEncode(categoryNamespace); Request request = new(encoded); diff --git a/CreamInstaller/Epic/GraphQL/Request.cs b/CreamInstaller/Epic/GraphQL/Request.cs index ff937b9..1d76786 100644 --- a/CreamInstaller/Epic/GraphQL/Request.cs +++ b/CreamInstaller/Epic/GraphQL/Request.cs @@ -13,6 +13,21 @@ internal class Request [JsonProperty(PropertyName = "query")] private string _gqlQuery => @"query searchOffers($namespace: String!) { Catalog { + searchStore(category: ""*"", namespace: $namespace){ + elements { + id + title + developer + items { + id + } + catalogNs { + mappings(pageType: ""productHome"") { + pageSlug + } + } + } + } catalogOffers( namespace: $namespace params: { @@ -20,6 +35,7 @@ internal class Request } ) { elements { + id title keyImages { type @@ -27,6 +43,7 @@ internal class Request } items { id + title developer } catalogNs { @@ -52,15 +69,6 @@ internal class Request 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; } diff --git a/CreamInstaller/Epic/GraphQL/Response.cs b/CreamInstaller/Epic/GraphQL/Response.cs index 53f01ce..b4a6924 100644 --- a/CreamInstaller/Epic/GraphQL/Response.cs +++ b/CreamInstaller/Epic/GraphQL/Response.cs @@ -17,10 +17,19 @@ public class Data public class Catalog { + [JsonProperty(PropertyName = "searchStore")] + public SearchStore SearchStore { get; protected set; } + [JsonProperty(PropertyName = "catalogOffers")] public CatalogOffers CatalogOffers { get; protected set; } } +public class SearchStore +{ + [JsonProperty(PropertyName = "elements")] + public Element[] Elements { get; protected set; } +} + public class CatalogOffers { [JsonProperty(PropertyName = "elements")] @@ -29,6 +38,9 @@ public class CatalogOffers public class Element { + [JsonProperty(PropertyName = "id")] + public string Id { get; protected set; } + [JsonProperty(PropertyName = "title")] public string Title { get; protected set; } @@ -47,6 +59,9 @@ public class Item [JsonProperty(PropertyName = "id")] public string Id { get; protected set; } + [JsonProperty(PropertyName = "title")] + public string Title { get; protected set; } + [JsonProperty(PropertyName = "developer")] public string Developer { get; protected set; } } diff --git a/CreamInstaller/InstallForm.cs b/CreamInstaller/InstallForm.cs index 6587bcc..cf00e42 100644 --- a/CreamInstaller/InstallForm.cs +++ b/CreamInstaller/InstallForm.cs @@ -53,22 +53,22 @@ internal partial class InstallForm : CustomForm } } - internal static void WriteCreamConfiguration(StreamWriter writer, string steamAppId, string name, SortedList steamDlcApps, InstallForm installForm = null) + internal static void WriteCreamConfiguration(StreamWriter writer, string appId, string name, SortedList dlc, InstallForm installForm = null) { writer.WriteLine($"; {name}"); writer.WriteLine("[steam]"); - writer.WriteLine($"appid = {steamAppId}"); + writer.WriteLine($"appid = {appId}"); writer.WriteLine(); 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) + installForm.UpdateUser($"Added game to cream_api.ini with appid {appId} ({name})", InstallationLog.Resource, info: false); + foreach (KeyValuePair pair in dlc) { - string appId = pair.Key; - (string dlcName, _) = pair.Value; - writer.WriteLine($"{appId} = {dlcName}"); + string dlcId = pair.Key; + (_, string dlcName, _) = pair.Value; + writer.WriteLine($"{dlcId} = {dlcName}"); if (installForm is not null) - installForm.UpdateUser($"Added DLC to cream_api.ini with appid {appId} ({dlcName})", InstallationLog.Resource, info: false); + installForm.UpdateUser($"Added DLC to cream_api.ini with appid {dlcId} ({dlcName})", InstallationLog.Resource, info: false); } } @@ -140,13 +140,13 @@ internal partial class InstallForm : CustomForm StreamWriter writer = new(cApi, true, Encoding.UTF8); if (selection.Id != "ParadoxLauncher") WriteCreamConfiguration(writer, selection.Id, selection.Name, selection.SelectedDlc, installForm); - foreach (Tuple> extraAppDlc in selection.ExtraDlc) + 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 dlcApps, InstallForm installForm = null) + internal static void WriteScreamConfiguration(StreamWriter writer, SortedList dlc, InstallForm installForm = null) { writer.WriteLine("{"); writer.WriteLine(" \"version\": 2,"); @@ -154,34 +154,54 @@ internal partial class InstallForm : CustomForm writer.WriteLine(" \"eos_logging\": false,"); writer.WriteLine(" \"block_metrics\": false,"); writer.WriteLine(" \"catalog_items\": {"); - writer.WriteLine(" \"unlock_all\": true,"); //writer.WriteLine(" \"unlock_all\": false,"); - writer.WriteLine(" \"override\": []"); //writer.WriteLine(" \"override\": ["); - /*KeyValuePair last = dlcApps.Last(); - foreach (KeyValuePair pair in dlcApps) + IEnumerable> catalogItems = dlc.Where(pair => pair.Value.type == DlcType.CatalogItem); + if (catalogItems.Any()) { - 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(" \"unlock_all\": false,"); + writer.WriteLine(" \"override\": ["); + KeyValuePair lastCatalogItem = catalogItems.Last(); + foreach (KeyValuePair pair in catalogItems) + { + string id = pair.Key; + (_, string name, _) = pair.Value; + writer.WriteLine($" \"{id}\"{(pair.Equals(lastCatalogItem) ? "" : ",")}"); + if (installForm is not null) + installForm.UpdateUser($"Added catalog item to ScreamAPI.json with id {id} ({name})", InstallationLog.Resource, info: false); + } + writer.WriteLine(" ]"); + } + else + { + writer.WriteLine(" \"unlock_all\": true,"); + writer.WriteLine(" \"override\": []"); } - writer.WriteLine(" ]");*/ writer.WriteLine(" },"); writer.WriteLine(" \"entitlements\": {"); - writer.WriteLine(" \"unlock_all\": true,"); //writer.WriteLine(" \"unlock_all\": false,"); - writer.WriteLine(" \"auto_inject\": true,"); //writer.WriteLine(" \"auto_inject\": false,"); - writer.WriteLine(" \"inject\": []"); //writer.WriteLine(" \"inject\": ["); - /*foreach (KeyValuePair pair in dlcApps) + IEnumerable> entitlements = dlc.Where(pair => pair.Value.type == DlcType.Entitlement); + if (entitlements.Any()) { - string id = pair.Key; - (string name, _) = pair.Value; - writer.WriteLine($" \"{id}\"{(pair.Equals(last) ? "" : ",")}"); + writer.WriteLine(" \"unlock_all\": false,"); + writer.WriteLine(" \"auto_inject\": false,"); + writer.WriteLine(" \"inject\": ["); + KeyValuePair lastEntitlement = entitlements.Last(); + foreach (KeyValuePair pair in entitlements) + { + string id = pair.Key; + (_, string name, _) = pair.Value; + writer.WriteLine($" \"{id}\"{(pair.Equals(lastEntitlement) ? "" : ",")}"); + if (installForm is not null) + installForm.UpdateUser($"Added entitlement to ScreamAPI.json with id {id} ({name})", InstallationLog.Resource, info: false); + } + writer.WriteLine(" ]"); + } + else + { + writer.WriteLine(" \"unlock_all\": true,"); + writer.WriteLine(" \"auto_inject\": true,"); + writer.WriteLine(" \"inject\": []"); } - writer.WriteLine(" ]");*/ writer.WriteLine(" }"); writer.WriteLine("}"); - if (installForm is not null) - installForm.UpdateUser($"Created 'unlock_all: true' ScreamAPI.json configuration (temporary until I figure out how to properly get all DLC ids)", InstallationLog.Resource, info: false); } internal static async Task UninstallScreamAPI(string directory, InstallForm installForm = null) => await Task.Run(() => @@ -252,7 +272,7 @@ internal partial class InstallForm : CustomForm StreamWriter writer = new(sApi, true, Encoding.UTF8); if (selection.Id != "ParadoxLauncher") WriteScreamConfiguration(writer, selection.SelectedDlc, installForm); - foreach (Tuple> extraAppDlc in selection.ExtraDlc) + foreach (Tuple> extraAppDlc in selection.ExtraDlc) WriteScreamConfiguration(writer, extraAppDlc.Item3, installForm); writer.Flush(); writer.Close(); diff --git a/CreamInstaller/ProgramSelection.cs b/CreamInstaller/ProgramSelection.cs index 2f0b5af..d206712 100644 --- a/CreamInstaller/ProgramSelection.cs +++ b/CreamInstaller/ProgramSelection.cs @@ -7,6 +7,13 @@ using Gameloop.Vdf.Linq; namespace CreamInstaller; +internal enum DlcType +{ + Default = 0, + CatalogItem = 1, + Entitlement = 2 +} + internal class ProgramSelection { internal bool Enabled = false; @@ -16,7 +23,6 @@ internal class ProgramSelection internal string Name = "Program"; internal string ProductUrl = null; - internal string IconUrl = null; internal string ClientIconUrl = null; @@ -28,9 +34,9 @@ internal class ProgramSelection internal bool IsSteam = false; internal VProperty AppInfo = null; - internal readonly SortedList AllDlc = new(); - internal readonly SortedList SelectedDlc = new(); - internal readonly List>> ExtraDlc = new(); + internal readonly SortedList AllDlc = new(); + internal readonly SortedList SelectedDlc = new(); + internal readonly List>> ExtraDlc = new(); // for Paradox Launcher internal bool AreDllsLocked { @@ -57,19 +63,19 @@ internal class ProgramSelection } } - private void Toggle(string dlcAppId, (string name, string icon) dlcApp, bool enabled) + private void Toggle(string dlcAppId, (DlcType type, string name, string icon) dlcApp, bool enabled) { if (enabled) SelectedDlc[dlcAppId] = dlcApp; else SelectedDlc.Remove(dlcAppId); } - internal void ToggleDlc(string dlcAppId, bool enabled) + internal void ToggleDlc(string dlcId, bool enabled) { - foreach (KeyValuePair pair in AllDlc) + foreach (KeyValuePair pair in AllDlc) { string appId = pair.Key; - (string name, string icon) dlcApp = pair.Value; - if (appId == dlcAppId) + (DlcType type, string name, string icon) dlcApp = pair.Value; + if (appId == dlcId) { Toggle(appId, dlcApp, enabled); break; @@ -78,18 +84,6 @@ internal class ProgramSelection 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 icon) dlcApp = pair.Value; - Toggle(appId, dlcApp, enabled); - } - Enabled = SelectedDlc.Any(); - } - internal ProgramSelection() => All.Add(this); internal void Validate() @@ -118,13 +112,13 @@ internal class ProgramSelection internal static List AllUsableEnabled => AllUsable.FindAll(s => s.Enabled); - internal static ProgramSelection FromId(string id) => AllUsable.Find(s => s.Id == id); + internal static ProgramSelection FromId(string gameId) => AllUsable.Find(s => s.Id == gameId); - internal static (string gameAppId, (string name, string icon) app)? GetDlcFromId(string appId) + internal static (string gameId, (DlcType type, string name, string icon) app)? GetDlcFromId(string dlcId) { foreach (ProgramSelection selection in AllUsable) - foreach (KeyValuePair pair in selection.AllDlc) - if (pair.Key == appId) return (selection.Id, pair.Value); + foreach (KeyValuePair pair in selection.AllDlc) + if (pair.Key == dlcId) return (selection.Id, pair.Value); return null; } } diff --git a/CreamInstaller/SelectForm.cs b/CreamInstaller/SelectForm.cs index a85fea8..8864534 100644 --- a/CreamInstaller/SelectForm.cs +++ b/CreamInstaller/SelectForm.cs @@ -140,7 +140,7 @@ internal partial class SelectForm : CustomForm return; } if (Program.Canceled) return; - ConcurrentDictionary dlc = new(); + ConcurrentDictionary dlc = new(); List dlcTasks = new(); List dlcIds = await SteamCMD.ParseDlcAppIds(appInfo); await SteamStore.ParseDlcAppIds(appId, dlcIds); @@ -167,7 +167,7 @@ internal partial class SelectForm : CustomForm } if (Program.Canceled) return; if (!string.IsNullOrWhiteSpace(dlcName)) - dlc[dlcAppId] = (dlcName, dlcIconStaticId); + dlc[dlcAppId] = (DlcType.Default, dlcName, dlcIconStaticId); RemoveFromRemainingDLCs(dlcAppId); progress.Report(++CompleteTasks); }); @@ -190,7 +190,7 @@ internal partial class SelectForm : CustomForm } selection ??= new(); - if (allCheckBox.Checked) selection.Enabled = true; + selection.Enabled = allCheckBox.Checked || selection.SelectedDlc.Any(); selection.Usable = true; selection.Id = appId; selection.Name = name; @@ -213,11 +213,11 @@ internal partial class SelectForm : CustomForm programNode.Checked = selection.Enabled; programNode.Remove(); selectionTreeView.Nodes.Add(programNode); - foreach (KeyValuePair pair in dlc) + foreach (KeyValuePair pair in dlc) { if (Program.Canceled || programNode is null) return; string appId = pair.Key; - (string name, string icon) dlcApp = pair.Value; + (DlcType type, string name, string icon) dlcApp = pair.Value; selection.AllDlc[appId] = dlcApp; if (allCheckBox.Checked) selection.SelectedDlc[appId] = dlcApp; TreeNode dlcNode = TreeNodes.Find(s => s.Name == appId) ?? new(); @@ -243,7 +243,6 @@ internal partial class SelectForm : CustomForm foreach (Manifest manifest in epicGames) { string @namespace = manifest.CatalogNamespace; - string mainId = manifest.MainGameCatalogItemId; string name = manifest.DisplayName; string directory = manifest.InstallLocation; ProgramSelection selection = ProgramSelection.FromId(@namespace); @@ -260,19 +259,19 @@ internal partial class SelectForm : CustomForm return; } if (Program.Canceled) return; - ConcurrentDictionary dlc = new(); + ConcurrentDictionary entitlements = new(); List dlcTasks = new(); - List<(string id, string name, string product, string icon, string developer)> dlcIds = await EpicStore.ParseDlcIds(@namespace); - if (dlcIds.Count > 0) + List<(string id, string name, string product, string icon, string developer)> entitlementIds = await EpicStore.QueryEntitlements(manifest); + if (entitlementIds.Any()) { - foreach ((string id, string name, string product, string icon, string developer) in dlcIds) + foreach ((string id, string name, string product, string icon, string developer) in entitlementIds) { if (Program.Canceled) return; AddToRemainingDLCs(id); Task task = Task.Run(() => { if (Program.Canceled) return; - dlc[id] = (name, product, icon, developer); + entitlements[id] = (name, product, icon, developer); RemoveFromRemainingDLCs(id); progress.Report(++CompleteTasks); }); @@ -282,7 +281,7 @@ internal partial class SelectForm : CustomForm Thread.Sleep(10); // to reduce control & window freezing } } - else + if (/*!catalogItems.Any() && */!entitlements.Any()) { RemoveFromRemainingGames(name); return; @@ -295,13 +294,13 @@ internal partial class SelectForm : CustomForm } selection ??= new(); - if (allCheckBox.Checked) selection.Enabled = true; + selection.Enabled = allCheckBox.Checked || selection.SelectedDlc.Any(); selection.Usable = true; selection.Id = @namespace; selection.Name = name; selection.RootDirectory = directory; selection.DllDirectories = dllDirectories; - foreach (KeyValuePair pair in dlc) + foreach (KeyValuePair pair in entitlements) if (pair.Value.name == selection.Name) { selection.ProductUrl = "https://www.epicgames.com/store/product/" + pair.Value.product; @@ -319,20 +318,35 @@ internal partial class SelectForm : CustomForm programNode.Checked = selection.Enabled; programNode.Remove(); selectionTreeView.Nodes.Add(programNode); - /*foreach (KeyValuePair pair in dlc) + /*TreeNode catalogItemsNode = TreeNodes.Find(s => s.Name == @namespace + "_catalogItems") ?? new(); + catalogItemsNode.Name = @namespace + "_catalogItems"; + catalogItemsNode.Text = "Catalog Items"; + catalogItemsNode.Checked = selection.SelectedDlc.Any(pair => pair.Value.type == DlcType.CatalogItem); + catalogItemsNode.Remove(); + programNode.Nodes.Add(catalogItemsNode);*/ + if (entitlements.Any()) { - if (Program.Canceled || programNode is null) return; - string dlcId = pair.Key; - (string name, string icon) dlcApp = (pair.Value.name, pair.Value.icon); - 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 = dlcApp.name; - dlcNode.Checked = selection.SelectedDlc.ContainsKey(dlcId); - dlcNode.Remove(); - programNode.Nodes.Add(dlcNode); - }*/ + /*TreeNode entitlementsNode = TreeNodes.Find(s => s.Name == @namespace + "_entitlements") ?? new(); + entitlementsNode.Name = @namespace + "_entitlements"; + entitlementsNode.Text = "Entitlements"; + entitlementsNode.Checked = selection.SelectedDlc.Any(pair => pair.Value.type == DlcType.Entitlement); + entitlementsNode.Remove(); + programNode.Nodes.Add(entitlementsNode);*/ + foreach (KeyValuePair pair in entitlements) + { + if (Program.Canceled || programNode is null/* || entitlementsNode is null*/) return; + string dlcId = pair.Key; + (DlcType type, string name, string icon) dlcApp = (DlcType.Entitlement, pair.Value.name, pair.Value.icon); + 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 = dlcApp.name; + dlcNode.Checked = selection.SelectedDlc.ContainsKey(dlcId); + dlcNode.Remove(); + programNode.Nodes.Add(dlcNode); //entitlementsNode.Nodes.Add(dlcNode); + } + } }); if (Program.Canceled) return; RemoveFromRemainingGames(name); @@ -395,7 +409,8 @@ internal partial class SelectForm : CustomForm ProgramSelection.ValidateAll(); TreeNodes.ForEach(node => { - if (!int.TryParse(node.Name, out int appId) || node.Parent is null && ProgramSelection.FromId(node.Name) is null) node.Remove(); + if (node.Parent is null && ProgramSelection.FromId(node.Name) is null) + node.Remove(); }); await GetApplicablePrograms(iProgress); await SteamCMD.Cleanup(); @@ -422,37 +437,47 @@ internal partial class SelectForm : CustomForm { if (e.Action == TreeViewAction.Unknown) return; TreeNode node = e.Node; - if (node is not null) - { - string appId = node.Name; - ProgramSelection selection = ProgramSelection.FromId(appId); - if (selection is null) - { - TreeNode parent = node.Parent; - if (parent is not null) - { - string gameAppId = parent.Name; - ProgramSelection.FromId(gameAppId).ToggleDlc(appId, node.Checked); - parent.Checked = parent.Nodes.Cast().ToList().Any(treeNode => treeNode.Checked); - } - } - else - { - if (selection.AllDlc.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; - } - } + if (node is null) return; + CheckNode(node); + SyncNodeParents(node); + SyncNodeDescendants(node); + allCheckBox.CheckedChanged -= OnAllCheckBoxChanged; + allCheckBox.Checked = TreeNodes.TrueForAll(treeNode => treeNode.Checked); + allCheckBox.CheckedChanged += OnAllCheckBoxChanged; installButton.Enabled = ProgramSelection.AllUsableEnabled.Any(); uninstallButton.Enabled = installButton.Enabled; } + private static void SyncNodeParents(TreeNode node) + { + TreeNode parentNode = node.Parent; + if (parentNode is not null) + { + parentNode.Checked = parentNode.Nodes.Cast().ToList().Any(childNode => childNode.Checked); + SyncNodeParents(parentNode); + } + } + + private static void SyncNodeDescendants(TreeNode node) => + node.Nodes.Cast().ToList().ForEach(childNode => + { + childNode.Checked = node.Checked; + CheckNode(childNode); + SyncNodeDescendants(childNode); + }); + + private static void CheckNode(TreeNode node) + { + (string gameId, (DlcType type, string name, string icon) app)? dlc = ProgramSelection.GetDlcFromId(node.Name); + if (dlc.HasValue) + { + (string gameId, _) = dlc.Value; + ProgramSelection selection = ProgramSelection.FromId(gameId); + if (selection is not null) + selection.ToggleDlc(node.Name, node.Checked); + } + } + internal List TreeNodes => GatherTreeNodes(selectionTreeView.Nodes); private List GatherTreeNodes(TreeNodeCollection nodeCollection) { @@ -547,14 +572,14 @@ internal partial class SelectForm : CustomForm selectionTreeView.NodeMouseClick += (sender, e) => { TreeNode node = e.Node; - TreeNode parentNode = node.Parent; + if (!node.Bounds.Contains(e.Location) || e.Button != MouseButtons.Right) return; + selectionTreeView.SelectedNode = node; string id = node.Name; ProgramSelection selection = ProgramSelection.FromId(id); - (string gameAppId, (string name, string icon) app)? dlc = null; + (string gameAppId, (DlcType type, string name, string icon) app)? dlc = null; if (selection is null) dlc = ProgramSelection.GetDlcFromId(id); - if (e.Button == MouseButtons.Right && node.Bounds.Contains(e.Location)) + if (selection is not null || dlc is not null) { - selectionTreeView.SelectedNode = node; nodeContextMenu.Items.Clear(); ToolStripMenuItem header = new(selection?.Name ?? node.Text, Image("Icon_" + id)); if (header.Image is null)