diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 19153e2e84..e5cadd5bce 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Generic; +using System.Linq; using log4net; @@ -53,6 +54,11 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (exportFileType == null) { var installed = new SortedDictionary(registry.Installed()); + var upgradeable = registry + .CheckUpgradeable(instance.VersionCriteria(), new HashSet()) + [true] + .Select(m => m.identifier) + .ToHashSet(); foreach (KeyValuePair mod in installed) { @@ -102,7 +108,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - else if (latest.version.IsEqualTo(current_version) && !registry.HasUpdate(mod.Key, instance.VersionCriteria())) + else if (!upgradeable.Contains(mod.Key)) { // Up to date log.InfoFormat("Latest {0} is {1}", mod.Key, latest.version); @@ -119,7 +125,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - else if (latest.version.IsGreaterThan(mod.Value) || registry.HasUpdate(mod.Key, instance.VersionCriteria())) + else { // Upgradable bullet = "^"; diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index 255d6b2be1..1f94d67603 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -202,7 +202,7 @@ private string[] GetInstIdentifiers(string prefix) CKAN.GameInstance inst = MainClass.GetGameInstance(manager); var registry = RegistryManager.Instance(inst, repoData).registry; return registry.Installed(false, false) - .Select(kvp => kvp.Key) + .Keys .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase) && !registry.GetInstalledVersion(ident).IsDLC) .ToArray(); @@ -214,7 +214,7 @@ private static bool WantsGameInstances(TypeInfo ti) private string[] GetGameInstances(string prefix) => manager.Instances - .Select(kvp => kvp.Key) + .Keys .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) .ToArray(); diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 5d99c7feb2..11ce3a499c 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -4,10 +4,10 @@ using System.Transactions; using Autofac; -using log4net; using CKAN.Versioning; using CKAN.Configuration; +using CKAN.Extensions; namespace CKAN.CmdLine { @@ -114,38 +114,17 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) var registry = regMgr.registry; if (options.upgrade_all) { - var to_upgrade = new List(); - - foreach (KeyValuePair mod in registry.Installed(false)) + var to_upgrade = registry + .CheckUpgradeable(instance.VersionCriteria(), new HashSet()) + [true]; + if (to_upgrade.Count == 0) { - try - { - // Check if upgrades are available - var latest = registry.LatestAvailable(mod.Key, instance.VersionCriteria()); - - // This may be an unindexed mod. If so, - // skip rather than crash. See KSP-CKAN/CKAN#841. - if (latest == null || latest.IsDLC) - { - continue; - } - - if (latest.version.IsGreaterThan(mod.Value) || registry.HasUpdate(mod.Key, instance.VersionCriteria())) - { - // Upgradable - log.InfoFormat("New version {0} found for {1}", - latest.version, latest.identifier); - to_upgrade.Add(latest); - } - - } - catch (ModuleNotFoundKraken) - { - log.InfoFormat("{0} is installed, but no longer in the registry", - mod.Key); - } + user.RaiseMessage(Properties.Resources.UpgradeAllUpToDate); + } + else + { + UpgradeModules(manager, user, instance, to_upgrade); } - UpgradeModules(manager, user, instance, true, to_upgrade); } else { @@ -193,14 +172,18 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) /// IUser object for output /// Game instance to use /// List of modules to upgrade - private void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, bool ConfirmPrompt, List modules) + private void UpgradeModules(GameInstanceManager manager, + IUser user, + CKAN.GameInstance instance, + List modules) { - UpgradeModules(manager, user, instance, repoData, + UpgradeModules( + manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, - ref possibleConfigOnlyDirs, regMgr, true, true, ConfirmPrompt), - m => modules.Add(m) - ); + ref possibleConfigOnlyDirs, + regMgr, true, true, true), + m => modules.Add(m)); } /// @@ -210,23 +193,80 @@ private void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameIn /// IUser object for output /// Game instance to use /// List of identifier[=version] to upgrade - private void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, List identsAndVersions) + private void UpgradeModules(GameInstanceManager manager, + IUser user, + CKAN.GameInstance instance, + List identsAndVersions) { - UpgradeModules(manager, user, instance, repoData, + UpgradeModules( + manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => - installer.Upgrade( - identsAndVersions.Select(arg => CkanModule.FromIDandVersion( - regMgr.registry, arg, - instance.VersionCriteria())) - .ToList(), - downloader, - ref possibleConfigOnlyDirs, - regMgr, - true), - m => identsAndVersions.Add(m.identifier) - ); + { + var crit = instance.VersionCriteria(); + var registry = regMgr.registry; + // Installed modules we're NOT upgrading + var heldIdents = registry.Installed(false) + .Keys + .Except(identsAndVersions.Select(arg => UpToFirst(arg, '='))) + .ToHashSet(); + // The modules we'll have after upgrading as aggressively as possible + var limiters = identsAndVersions.Select(req => CkanModule.FromIDandVersion(registry, req, crit) + ?? DefaultIfThrows( + () => registry.LatestAvailable(req, crit)) + ?? registry.GetInstalledVersion(req)) + .Concat(heldIdents.Select(ident => registry.GetInstalledVersion(ident))) + .Where(m => m != null) + .ToList(); + // Modules allowed by THOSE modules' relationships + var upgradeable = registry + .CheckUpgradeable(crit, heldIdents, limiters) + [true] + .ToDictionary(m => m.identifier, + m => m); + // Substitute back in the ident=ver requested versions + var to_upgrade = new List(); + foreach (var request in identsAndVersions) + { + var module = CkanModule.FromIDandVersion(registry, request, crit) + ?? (upgradeable.TryGetValue(request, out CkanModule m) + ? m + : null); + if (module == null) + { + user.RaiseMessage(Properties.Resources.UpgradeAlreadyUpToDate, request); + } + else + { + to_upgrade.Add(module); + } + } + if (to_upgrade.Count > 0) + { + installer.Upgrade(to_upgrade, downloader, ref possibleConfigOnlyDirs, regMgr, true); + } + }, + m => identsAndVersions.Add(m.identifier)); } + public static T DefaultIfThrows(Func func) + { + try + { + return func(); + } + catch + { + return default; + } + } + + private static string UpToFirst(string orig, char toFind) + => UpTo(orig, orig.IndexOf(toFind)); + + private static string UpTo(string orig, int pos) + => pos >= 0 && pos < orig.Length ? orig.Substring(0, pos) + : orig; + // Action isn't allowed private delegate void AttemptUpgradeAction(ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs); @@ -241,11 +281,12 @@ private void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameIn /// Game instance to use /// Function to call to try to perform the actual upgrade, may throw TooManyModsProvideKraken /// Function to call when the user has requested a new module added to the change set in response to TooManyModsProvideKraken - private void UpgradeModules( - GameInstanceManager manager, IUser user, CKAN.GameInstance instance, - RepositoryDataManager repoData, - AttemptUpgradeAction attemptUpgradeCallback, - Action addUserChoiceCallback) + private void UpgradeModules(GameInstanceManager manager, + IUser user, + CKAN.GameInstance instance, + RepositoryDataManager repoData, + AttemptUpgradeAction attemptUpgradeCallback, + Action addUserChoiceCallback) { using (TransactionScope transact = CkanTransaction.CreateTransactionScope()) { var installer = new ModuleInstaller(instance, manager.Cache, user); @@ -282,7 +323,5 @@ private void UpgradeModules( private readonly IUser user; private readonly GameInstanceManager manager; private readonly RepositoryDataManager repoData; - - private static readonly ILog log = LogManager.GetLogger(typeof(Upgrade)); } } diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx index 1dcf548b05..de8b154718 100644 --- a/Cmdline/Properties/Resources.resx +++ b/Cmdline/Properties/Resources.resx @@ -372,6 +372,8 @@ Try `ckan list` for a list of installed mods. You already have the latest version Upgrade aborted: {0} Module {0} not found + Module {0} is already up to date + All modules are up to date CKAN can't upgrade expansion '{0}' for you To upgrade this expansion, download any updates from the store page from which you purchased it: {0} diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index 83d42663c5..6816819f0a 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -48,13 +48,14 @@ public override void Run(ConsoleTheme theme, Action process = null // Reset this so we stop unless an exception sets it to true retry = false; - RegistryManager regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + var regMgr = RegistryManager.Instance(manager.CurrentInstance, repoData); + var registry = regMgr.registry; // GUI prompts user to choose recs/sugs, // CmdLine assumes recs and ignores sugs if (plan.Install.Count > 0) { // Track previously rejected optional dependencies and don't prompt for them again. - DependencyScreen ds = new DependencyScreen(manager, regMgr.registry, plan, rejected, debug); + DependencyScreen ds = new DependencyScreen(manager, registry, plan, rejected, debug); if (ds.HaveOptions()) { LaunchSubScreen(theme, ds); } @@ -74,16 +75,27 @@ public override void Run(ConsoleTheme theme, Action process = null } NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(this, manager.Cache); if (plan.Install.Count > 0) { - List iList = new List(plan.Install); + var iList = plan.Install + .Select(m => registry.LatestAvailable(m.identifier, + manager.CurrentInstance.VersionCriteria(), + null, + registry.InstalledModules + .Select(im => im.Module) + .ToArray(), + plan.Install)) + .ToArray(); inst.InstallList(iList, resolvOpts, regMgr, ref possibleConfigOnlyDirs, dl); plan.Install.Clear(); } if (plan.Upgrade.Count > 0) { - inst.Upgrade(plan.Upgrade - .Select(ident => regMgr.registry.LatestAvailable( - ident, manager.CurrentInstance.VersionCriteria())) - .ToList(), - dl, ref possibleConfigOnlyDirs, regMgr); + var upgGroups = registry + .CheckUpgradeable(manager.CurrentInstance.VersionCriteria(), + // Hold identifiers not chosen for upgrading + registry.Installed(false) + .Keys + .Except(plan.Upgrade) + .ToHashSet()); + inst.Upgrade(upgGroups[true], dl, ref possibleConfigOnlyDirs, regMgr); plan.Upgrade.Clear(); } if (plan.Replace.Count > 0) { diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index 3e55db34df..de4e26f761 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -266,6 +266,9 @@ private int addDependencies(int top = 8) const int lblW = 16; int nameW = midL - 2 - lblW - 2 - 1; int depsH = (h - 2) * numDeps / (numDeps + numConfs); + var upgradeableGroups = registry + .CheckUpgradeable(manager.CurrentInstance.VersionCriteria(), + new HashSet()); AddObject(new ConsoleFrame( 1, top, midL, top + h - 1, @@ -290,7 +293,8 @@ private int addDependencies(int top = 8) foreach (RelationshipDescriptor rd in mod.depends) { tb.AddLine(ScreenObject.TruncateLength( // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString())) + ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), + upgradeableGroups[true])) + rd.ToString(), nameW )); @@ -315,7 +319,8 @@ private int addDependencies(int top = 8) foreach (RelationshipDescriptor rd in mod.conflicts) { tb.AddLine(ScreenObject.TruncateLength( // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString())) + ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), + upgradeableGroups[true])) + rd.ToString(), nameW )); diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 39c2708086..838da7a63f 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -82,7 +82,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re case "i": return registry.IsInstalled(m.identifier, false); case "u": - return registry.HasUpdate(m.identifier, manager.CurrentInstance.VersionCriteria()); + return upgradeableGroups?[true].Any(upg => upg.identifier == m.identifier) ?? false; case "n": // Filter new return recent.Contains(m.identifier); @@ -188,7 +188,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re ); moduleList.AddTip("+", Properties.Resources.ModListUpgradeTip, () => moduleList.Selection != null && !moduleList.Selection.IsDLC - && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) + && (upgradeableGroups?[true].Any(upg => upg.identifier == moduleList.Selection.identifier) ?? false) ); moduleList.AddTip("+", Properties.Resources.ModListReplaceTip, () => moduleList.Selection != null @@ -199,7 +199,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re if (!registry.IsInstalled(moduleList.Selection.identifier, false)) { plan.ToggleInstall(moduleList.Selection); } else if (registry.IsInstalled(moduleList.Selection.identifier, false) - && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria())) { + && (upgradeableGroups?[true].Any(upg => upg.identifier == moduleList.Selection.identifier) ?? false)) { plan.ToggleUpgrade(moduleList.Selection); } else if (registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null) { plan.ToggleReplace(moduleList.Selection.identifier); @@ -367,21 +367,13 @@ private bool CaptureKey(ConsoleTheme theme) private bool HasAnyUpgradeable() { - foreach (string identifier in registry.Installed(true).Select(kvp => kvp.Key)) { - if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { - return true; - } - } - return false; + return (upgradeableGroups?[true].Count ?? 0) > 0; } private bool UpgradeAll(ConsoleTheme theme) { - foreach (string identifier in registry.Installed(true).Select(kvp => kvp.Key)) { - if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { - plan.Upgrade.Add(identifier); - } - } + plan.Upgrade.UnionWith(upgradeableGroups?[true].Select(m => m.identifier) + ?? Enumerable.Empty()); return true; } @@ -564,17 +556,20 @@ private List GetAllMods(ConsoleTheme theme, bool force = false) { UpdateRegistry(theme, false); } - allMods = new List(registry.CompatibleModules(manager.CurrentInstance.VersionCriteria())); + var crit = manager.CurrentInstance.VersionCriteria(); + allMods = new List(registry.CompatibleModules(crit)); foreach (InstalledModule im in registry.InstalledModules) { CkanModule m = null; try { - m = registry.LatestAvailable(im.identifier, manager.CurrentInstance.VersionCriteria()); + m = registry.LatestAvailable(im.identifier, crit); } catch (ModuleNotFoundKraken) { } if (m == null) { // Add unavailable installed mods to the list allMods.Add(im.Module); } } + upgradeableGroups = registry + .CheckUpgradeable(crit, new HashSet()); } return allMods; } @@ -621,7 +616,8 @@ private bool ApplyChanges(ConsoleTheme theme) /// public string StatusSymbol(CkanModule m) { - return StatusSymbol(plan.GetModStatus(manager, registry, m.identifier)); + return StatusSymbol(plan.GetModStatus(manager, registry, m.identifier, + upgradeableGroups?[true] ?? new List())); } /// @@ -664,8 +660,9 @@ private long totalInstalledDownloadSize() private readonly GameInstanceManager manager; private RegistryManager regMgr; private Registry registry; - private readonly RepositoryDataManager repoData; - private readonly bool debug; + private readonly RepositoryDataManager repoData; + private Dictionary> upgradeableGroups; + private readonly bool debug; private readonly ConsoleField searchBox; private readonly ConsoleListBox moduleList; @@ -773,15 +770,19 @@ public void Reset() /// Game instance manager containing the instances /// Registry of instance being displayed /// The mod + /// List of modules that can be upgraded /// /// Status of mod /// - public InstallStatus GetModStatus(GameInstanceManager manager, IRegistryQuerier registry, string identifier) + public InstallStatus GetModStatus(GameInstanceManager manager, + IRegistryQuerier registry, + string identifier, + List upgradeable) { if (registry.IsInstalled(identifier, false)) { if (Remove.Contains(identifier)) { return InstallStatus.Removing; - } else if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { + } else if (upgradeable.Any(m => m.identifier == identifier)) { if (Upgrade.Contains(identifier)) { return InstallStatus.Upgrading; } else { diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 378e5b3c38..d259660f2d 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -168,45 +168,99 @@ public static bool IsAutodetected(this IRegistryQuerier querier, string identifi && querier.InstalledVersion(identifier) is UnmanagedModuleVersion; /// - /// Is the mod installed and does it have a newer version compatible with version + /// Is the mod installed and does it have a newer version compatible with versionCrit /// - public static bool HasUpdate(this IRegistryQuerier querier, string identifier, GameVersionCriteria version) + public static bool HasUpdate(this IRegistryQuerier querier, + string identifier, + GameVersionCriteria versionCrit, + out CkanModule latestMod, + ICollection installed = null) { - CkanModule newest_version; + // Check if it's installed (including manually!) + var instVer = querier.InstalledVersion(identifier); + if (instVer == null) + { + latestMod = null; + return false; + } + // Check if it's available try { - // Check if it's both installed and available - newest_version = querier.LatestAvailable(identifier, version); - if (newest_version == null || !querier.IsInstalled(identifier, false)) + latestMod = querier.LatestAvailable(identifier, versionCrit, null, installed); + } + catch + { + latestMod = null; + } + if (latestMod == null) + { + return false; + } + // Check if the installed module is up to date + var comp = latestMod.version.CompareTo(instVer); + if (comp == -1 + || (comp == 0 && !querier.MetadataChanged(identifier))) + { + latestMod = null; + return false; + } + // Checking with a RelationshipResolver here would commit us to + // testing against the currently installed modules in the registry, + // which could block us from upgrading away from a problem. + // Trust our LatestAvailable call above. + return true; + } + + public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, + GameVersionCriteria versionCrit, + HashSet heldIdents) + { + // Get the absolute latest versions ignoring restrictions, + // to break out of mutual version-depending deadlocks + var unlimited = querier.Installed(false) + .Keys + .Select(ident => !heldIdents.Contains(ident) + && querier.HasUpdate(ident, versionCrit, + out CkanModule latest) + && !latest.IsDLC + ? latest + : querier.GetInstalledVersion(ident)) + .Where(m => m != null) + .ToList(); + return querier.CheckUpgradeable(versionCrit, heldIdents, unlimited); + } + + public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, + GameVersionCriteria versionCrit, + HashSet heldIdents, + List initial) + { + // Use those as the installed modules + var upgradeable = new List(); + var notUpgradeable = new List(); + foreach (var ident in initial.Select(module => module.identifier)) + { + if (!heldIdents.Contains(ident) + && querier.HasUpdate(ident, versionCrit, + out CkanModule latest, initial) + && !latest.IsDLC) { - return false; + upgradeable.Add(latest); } - // Check if the installed module is up to date - if (newest_version.version <= querier.InstalledVersion(identifier) - && !querier.MetadataChanged(identifier)) + else { - return false; - } - // All quick checks pass. Now check the relationships. - var instMod = querier.InstalledModule(identifier); - RelationshipResolver resolver = new RelationshipResolver( - new CkanModule[] { newest_version }, - // Remove the old module when installing the new one - instMod == null ? null : new CkanModule[] { instMod.Module }, - new RelationshipResolverOptions() + var current = querier.InstalledModule(ident); + if (current != null && !current.Module.IsDLC) { - with_recommends = false, - without_toomanyprovides_kraken = true, - }, - querier, - version - ); + notUpgradeable.Add(current.Module); + } + } } - catch (Exception) + return new Dictionary> { - return false; - } - return true; + { true, upgradeable }, + { false, notUpgradeable }, + }; } private static bool MetadataChanged(this IRegistryQuerier querier, string identifier) diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index cd777a7d17..67c4a84732 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -594,7 +594,7 @@ public bool ScanDlc() .Concat(WellKnownDlcScan()) .ToDictionary()); - private IEnumerable> TestDlcScan(string dlcDir) + private static IEnumerable> TestDlcScan(string dlcDir) => (Directory.Exists(dlcDir) ? Directory.EnumerateFiles(dlcDir, "*.dlc", SearchOption.TopDirectoryOnly) diff --git a/Core/Repositories/AvailableModule.cs b/Core/Repositories/AvailableModule.cs index 85c564e2bd..15d53aa593 100644 --- a/Core/Repositories/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -171,6 +171,20 @@ private static bool DependsAndConflictsOK(CkanModule module, ICollection exportOptions, out ExportOption se Filter = string.Join("|", exportOptions.Select(i => i.ToString()).ToArray()), Title = Properties.Resources.ExportInstalledModsDialogTitle }; - if (dlg.ShowDialog(Main.Instance) == DialogResult.OK) + if (dlg.ShowDialog(ParentForm) == DialogResult.OK) { selectedOption = exportOptions[dlg.FilterIndex - 1]; filename = dlg.FileName; diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index fa40800bd8..0f66e3aeed 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -79,6 +79,7 @@ public ManageMods() public event Action RaiseMessage; public event Action RaiseError; + public event Action SetStatusBar; public event Action ClearStatusBar; public event Action LaunchGame; public event Action EditCommandLines; @@ -88,20 +89,24 @@ private List SortColumns { get { - var configuration = Main.Instance.configuration; // Make sure we don't return any column the GUI doesn't know about. - var unknownCols = configuration.SortColumns.Where(col => !ModGrid.Columns.Contains(col)).ToList(); + var unknownCols = guiConfig.SortColumns.Where(col => !ModGrid.Columns.Contains(col)).ToList(); foreach (var unknownCol in unknownCols) { - int index = configuration.SortColumns.IndexOf(unknownCol); - configuration.SortColumns.RemoveAt(index); - configuration.MultiSortDescending.RemoveAt(index); + int index = guiConfig.SortColumns.IndexOf(unknownCol); + guiConfig.SortColumns.RemoveAt(index); + guiConfig.MultiSortDescending.RemoveAt(index); } - return configuration.SortColumns; + return guiConfig.SortColumns; } } - private List descending => Main.Instance.configuration.MultiSortDescending; + private GUIConfiguration guiConfig => Main.Instance.configuration; + private GameInstance currentInstance => Main.Instance.CurrentInstance; + private GameInstanceManager manager => Main.Instance.Manager; + private IUser user => Main.Instance.currentUser; + + private List descending => guiConfig.MultiSortDescending; public event Action OnSelectedModuleChanged; public event Action, Dictionary> OnChangeSetChanged; @@ -146,10 +151,9 @@ private void ChangeSetUpdated() } OnChangeSetChanged?.Invoke(ChangeSet, Conflicts); - var removing = (currentChangeSet ?? Enumerable.Empty()) - .Where(ch => ch?.ChangeType == GUIModChangeType.Remove) - .Select(ch => ch.Mod.identifier) - .ToHashSet(); + var removing = changeIdentifiersOfType(GUIModChangeType.Remove) + .Except(changeIdentifiersOfType(GUIModChangeType.Install)) + .ToHashSet(); foreach (var kvp in mainModList.full_list_of_mod_rows) { if (removing.Contains(kvp.Key)) @@ -166,6 +170,11 @@ private void ChangeSetUpdated() }); } + private IEnumerable changeIdentifiersOfType(GUIModChangeType changeType) + => (currentChangeSet ?? Enumerable.Empty()) + .Where(ch => ch?.ChangeType == changeType) + .Select(ch => ch.Mod.identifier); + private Dictionary Conflicts { get => conflicts; @@ -189,15 +198,14 @@ private void ConflictsUpdated(Dictionary prevConflicts) ClearStatusBar?.Invoke(); } - var inst = Main.Instance.CurrentInstance; - var registry = RegistryManager.Instance(inst, repoData).registry; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; if (prevConflicts != null) { // Mark old conflicts as non-conflicted // (rows that are _still_ conflicted will be marked as such in the next loop) foreach (GUIMod guiMod in prevConflicts.Keys) { - SetUnsetRowConflicted(guiMod, false, null, inst, registry); + SetUnsetRowConflicted(guiMod, false, null, currentInstance, registry); } } if (Conflicts != null) @@ -205,7 +213,7 @@ private void ConflictsUpdated(Dictionary prevConflicts) // Mark current conflicts as conflicted foreach ((GUIMod guiMod, string conflict_text) in Conflicts) { - SetUnsetRowConflicted(guiMod, true, conflict_text, inst, registry); + SetUnsetRowConflicted(guiMod, true, conflict_text, currentInstance, registry); } } } @@ -248,7 +256,7 @@ private void FilterToolButton_DropDown_Opening(object sender, CancelEventArgs e) private void FilterTagsToolButton_DropDown_Opening(object sender, CancelEventArgs e) { - var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; FilterTagsToolButton.DropDownItems.Clear(); foreach (var kvp in registry.Tags.OrderBy(kvp => kvp.Key)) { @@ -274,10 +282,10 @@ private void FilterTagsToolButton_DropDown_Opening(object sender, CancelEventArg private void FilterLabelsToolButton_DropDown_Opening(object sender, CancelEventArgs e) { FilterLabelsToolButton.DropDownItems.Clear(); - foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name)) + foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(currentInstance.Name)) { FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem( - $"{mlbl.Name} ({mlbl.ModuleCount(Main.Instance.CurrentInstance.game)})", + $"{mlbl.Name} ({mlbl.ModuleCount(currentInstance.game)})", null, customFilterButton_Click ) { @@ -296,13 +304,13 @@ private void LabelsContextMenuStrip_Opening(object sender, CancelEventArgs e) LabelsContextMenuStrip.Items.Clear(); var module = SelectedModule; - foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name)) + foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(currentInstance.Name)) { LabelsContextMenuStrip.Items.Add( new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click) { BackColor = mlbl.Color, - Checked = mlbl.ContainsModule(Main.Instance.CurrentInstance.game, module.Identifier), + Checked = mlbl.ContainsModule(currentInstance.game, module.Identifier), CheckOnClick = true, Tag = mlbl, } @@ -318,37 +326,37 @@ private void labelMenuItem_Click(object sender, EventArgs e) var item = sender as ToolStripMenuItem; var mlbl = item.Tag as ModuleLabel; var module = SelectedModule; - var inst = Main.Instance.CurrentInstance; if (item.Checked) { - mlbl.Add(inst.game, module.Identifier); + mlbl.Add(currentInstance.game, module.Identifier); } else { - mlbl.Remove(inst.game, module.Identifier); + mlbl.Remove(currentInstance.game, module.Identifier); } if (mlbl.HoldVersion) { UpdateAllToolButton.Enabled = mainModList.Modules.Any(mod => mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); } - var registry = RegistryManager.Instance(inst, repoData).registry; - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, + currentInstance.Name, currentInstance.game, registry); mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); UpdateHiddenTagsAndLabels(); } private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e) { - EditLabelsDialog eld = new EditLabelsDialog(Main.Instance.currentUser, Main.Instance.Manager, mainModList.ModuleLabels); + var eld = new EditLabelsDialog(user, manager, mainModList.ModuleLabels); eld.ShowDialog(this); eld.Dispose(); mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); - var inst = Main.Instance.CurrentInstance; - var registry = RegistryManager.Instance(inst, repoData).registry; - foreach (GUIMod module in mainModList.Modules) + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + foreach (var module in mainModList.Modules) { - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, + currentInstance.Name, currentInstance.game, registry); } UpdateHiddenTagsAndLabels(); } @@ -437,7 +445,7 @@ private void FilterAllButton_Click(object sender, EventArgs e) public void Filter(SavedSearch search, bool merge) { var searches = search.Values.Select(s => ModSearch.Parse(s, - mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name).ToList() + mainModList.ModuleLabels.LabelsFor(currentInstance.Name).ToList() )).ToList(); Util.Invoke(ModGrid, () => @@ -473,7 +481,7 @@ private void ShowHideColumns(List searches) if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol" && !installedColumnNames.Contains(col.Name)) { - col.Visible = !Main.Instance.configuration.HiddenColumnNames.Contains(col.Name); + col.Visible = !guiConfig.HiddenColumnNames.Contains(col.Name); } } @@ -490,7 +498,7 @@ private void ShowHideColumns(List searches) private void setInstalledColumnsVisible(bool visible) { - var hiddenColumnNames = Main.Instance.configuration.HiddenColumnNames; + var hiddenColumnNames = guiConfig.HiddenColumnNames; foreach (var colName in installedColumnNames.Where(nm => ModGrid.Columns.Contains(nm))) { ModGrid.Columns[colName].Visible = visible && !hiddenColumnNames.Contains(colName); @@ -504,20 +512,22 @@ public void MarkAllUpdates() { WithFrozenChangeset(() => { - foreach (var row in mainModList.full_list_of_mod_rows.Values) + foreach (var gmod in mainModList.full_list_of_mod_rows + .Values + .Select(row => row.Tag) + .OfType()) { - var mod = row.Tag as GUIMod; - if (mod?.HasUpdate ?? false) + if (gmod?.HasUpdate ?? false) { - if (!Main.Instance.LabelsHeld(mod.Identifier)) + if (!Main.Instance.LabelsHeld(gmod.Identifier)) { - mod.SetUpgradeChecked(row, UpdateCol, true); + gmod.SelectedMod = gmod.LatestAvailableMod; } } } // only sort by Update column if checkbox in settings checked - if (Main.Instance.configuration.AutoSortByUpdate) + if (guiConfig.AutoSortByUpdate) { // Retain their current sort as secondaries AddSort(UpdateCol, true); @@ -541,21 +551,9 @@ private void ApplyToolButton_Click(object sender, EventArgs e) StartChangeSet?.Invoke(currentChangeSet, Conflicts); } - public void MarkModForUpdate(string identifier, bool value) - { - Util.Invoke(this, () => _MarkModForUpdate(identifier, value)); - } - - private void _MarkModForUpdate(string identifier, bool value) - { - DataGridViewRow row = mainModList.full_list_of_mod_rows[identifier]; - var mod = (GUIMod)row.Tag; - mod.SetUpgradeChecked(row, UpdateCol, value); - } - private void LaunchGameToolStripMenuItem_MouseHover(object sender, EventArgs e) { - var cmdLines = Main.Instance.configuration.CommandLines; + var cmdLines = guiConfig.CommandLines; LaunchGameToolStripMenuItem.DropDownItems.Clear(); LaunchGameToolStripMenuItem.DropDownItems.AddRange( cmdLines.Select(cmdLine => (ToolStripItem) @@ -572,7 +570,7 @@ private void LaunchGameToolStripMenuItem_MouseHover(object sender, EventArgs e) } private string CmdLineHelp(string cmdLine) - => Main.Instance.Manager.SteamLibrary.Games.Length > 0 + => manager.SteamLibrary.Games.Length > 0 ? cmdLine.StartsWith("steam://", StringComparison.InvariantCultureIgnoreCase) ? Properties.Resources.ManageModsSteamPlayTimeYesTooltip : Properties.Resources.ManageModsSteamPlayTimeNoTooltip @@ -680,7 +678,7 @@ private void ShowHeaderContextMenu(bool columns = true, if (tags) { // Add tags - var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; ModListHeaderContextMenuStrip.Items.AddRange( registry.Tags.OrderBy(kvp => kvp.Key) .Select(kvp => new ToolStripMenuItem() @@ -709,7 +707,7 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, ToolStripI if (clickedItem?.Tag is DataGridViewColumn col) { col.Visible = !clickedItem.Checked; - Main.Instance.configuration.SetColumnVisibility(col.Name, !clickedItem.Checked); + guiConfig.SetColumnVisibility(col.Name, !clickedItem.Checked); if (col.Index == 0) { InstallAllCheckbox.Visible = col.Visible; @@ -857,52 +855,78 @@ private void ModGrid_CellMouseDoubleClick(object sender, DataGridViewCellMouseEv private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e) { - int row_index = e.RowIndex; - int column_index = e.ColumnIndex; - - if (row_index < 0 || column_index < 0) + if (e.RowIndex >= 0) { - return; - } - - DataGridView grid = sender as DataGridView; - DataGridViewRow row = grid?.Rows[row_index]; - DataGridViewCell gridCell = row?.Cells[column_index]; - - if (gridCell is DataGridViewLinkCell) - { - // Launch URLs if found in grid - DataGridViewLinkCell cell = gridCell as DataGridViewLinkCell; - string cmd = cell?.Value.ToString(); - if (!string.IsNullOrEmpty(cmd)) + var row = ModGrid?.Rows?[e.RowIndex]; + switch (row?.Cells[e.ColumnIndex]) { - Utilities.ProcessStartURL(cmd); + case DataGridViewLinkCell linkCell: + // Launch URLs if found in grid + string cmd = linkCell.Value.ToString(); + if (!string.IsNullOrEmpty(cmd)) + { + Utilities.ProcessStartURL(cmd); + } + break; + + case DataGridViewCheckBoxCell checkCell: + // checked is a keyword in C# + var nowChecked = (bool)checkCell.Value; + if (row?.Tag is GUIMod gmod) + { + switch (ModGrid.Columns[e.ColumnIndex].Name) + { + case "Installed": + gmod.SelectedMod = nowChecked ? gmod.InstalledMod?.Module + ?? gmod.LatestAvailableMod + : null; + break; + case "UpdateCol": + gmod.SelectedMod = nowChecked + ? gmod.SelectedMod != null + && gmod.InstalledMod.Module.version < gmod.SelectedMod.version + ? gmod.SelectedMod + : gmod.LatestAvailableMod + : gmod.InstalledMod?.Module; + break; + case "AutoInstalled": + gmod.SetAutoInstallChecked(row, AutoInstalled); + OnRegistryChanged?.Invoke(); + break; + } + } + break; } } - else + } + + private void guiModule_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (sender is GUIMod gmod + && mainModList.full_list_of_mod_rows.TryGetValue(gmod.Identifier, + out DataGridViewRow row)) { - if (row?.Tag is GUIMod gui_mod) + switch (e.PropertyName) { - switch (ModGrid.Columns[column_index].Name) - { - case "Installed": - gui_mod.SetInstallChecked(row, Installed); - // The above will call UpdateChangeSetAndConflicts, so we don't need to. - return; - case "AutoInstalled": - gui_mod.SetAutoInstallChecked(row, AutoInstalled); - OnRegistryChanged?.Invoke(); - break; - case "UpdateCol": - gui_mod.SetUpgradeChecked(row, UpdateCol); - break; - case "ReplaceCol": - gui_mod.SetReplaceChecked(row, ReplaceCol); - break; - } - var inst = Main.Instance.CurrentInstance; - UpdateChangeSetAndConflicts(inst, - RegistryManager.Instance(inst, repoData).registry); + case "SelectedMod": + if (row.Cells[Installed.Index] is DataGridViewCheckBoxCell instCell) + { + instCell.Value = gmod.SelectedMod != null; + } + if (row.Cells[UpdateCol.Index] is DataGridViewCheckBoxCell upgCell) + { + upgCell.Value = gmod.InstalledMod != null + && gmod.SelectedMod != null + && gmod.InstalledMod.Module.version < gmod.SelectedMod.version; + } + + // This call is needed to force the UI to update, + // otherwise the checkboxes can look checked when unchecked or vice versa + ModGrid.RefreshEdit(); + // Update the changeset + UpdateChangeSetAndConflicts(currentInstance, + RegistryManager.Instance(currentInstance, repoData).registry); + break; } } } @@ -921,33 +945,19 @@ public void RemoveChangesetItem(ModChange change) } else if (change.IsUserRequested) { + guiMod.SelectedMod = guiMod.InstalledMod?.Module; switch (change.ChangeType) { - case GUIModChangeType.Install: - if (guiMod.IsAutodetected) - { - guiMod.SetUpgradeChecked(row, UpdateCol, false); - } - else + case GUIModChangeType.Replace: + if (row.Cells[ReplaceCol.Index] is DataGridViewCheckBoxCell checkCell) { - guiMod.SetInstallChecked(row, Installed, false); - return; + checkCell.Value = false; } break; - case GUIModChangeType.Remove: - guiMod.SetInstallChecked(row, Installed, true); - return; - case GUIModChangeType.Update: - guiMod.SetUpgradeChecked(row, UpdateCol, false); - break; - case GUIModChangeType.Replace: - guiMod.SetReplaceChecked(row, ReplaceCol, false); - break; } } - var inst = Main.Instance.CurrentInstance; UpdateChangeSetAndConflicts( - inst, RegistryManager.Instance(inst, repoData).registry); + currentInstance, RegistryManager.Instance(currentInstance, repoData).registry); } } @@ -987,14 +997,7 @@ private void InstallAllCheckbox_CheckChanged(object sender, EventArgs e) { if (row.Tag is GUIMod gmod) { - if (gmod.IsUpgradeChecked) - { - gmod.SetUpgradeChecked(row, UpdateCol, false); - } - if (gmod.IsInstallChecked) - { - gmod.SetInstallChecked(row, Installed, false); - } + gmod.SelectedMod = null; } } } @@ -1007,20 +1010,14 @@ public void ClearChangeSet() { foreach (DataGridViewRow row in mainModList.full_list_of_mod_rows.Values) { - GUIMod mod = row.Tag as GUIMod; - if (mod.IsInstallChecked != mod.IsInstalled) + if (row.Tag is GUIMod gmod) { - mod.SetInstallChecked(row, Installed, mod.IsInstalled); + gmod.SelectedMod = gmod.InstalledMod?.Module; } - else if (mod.InstalledMod != null) + if (row.Cells[ReplaceCol.Index] is DataGridViewCheckBoxCell checkCell) { - var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; - mod.SelectedMod = registry.GetModuleByVersion( - mod.InstalledMod.identifier, mod.InstalledMod.Module.version) - ?? mod.InstalledMod.Module; + checkCell.Value = false; } - mod.SetUpgradeChecked(row, UpdateCol, false); - mod.SetReplaceChecked(row, ReplaceCol, false); } // Marking a mod as AutoInstalled can immediately queue it for removal if there is no dependent mod. // Reset the state of the AutoInstalled checkbox for these by deducing it from the changeset. @@ -1057,8 +1054,8 @@ private void WithFrozenChangeset(Action action) // Don't let anything ever prevent us from unfreezing the changeset freezeChangeSet = false; ModGrid.Refresh(); - var inst = Main.Instance.CurrentInstance; - UpdateChangeSetAndConflicts(inst, RegistryManager.Instance(inst, repoData).registry); + UpdateChangeSetAndConflicts(currentInstance, + RegistryManager.Instance(currentInstance, repoData).registry); } } } @@ -1186,7 +1183,7 @@ private void reinstallToolStripMenuItem_Click(object sender, EventArgs e) var module = SelectedModule?.ToModule(); if (module != null) { - IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; StartChangeSet?.Invoke(new List() { // "Upgrade" to latest metadata for same module version @@ -1212,11 +1209,11 @@ private void purgeContentsToolStripMenuItem_Click(object sender, EventArgs e) var selected = SelectedModule; if (selected != null) { - IRegistryQuerier registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; var allAvail = registry.AvailableByIdentifier(selected.Identifier); foreach (CkanModule mod in allAvail) { - Main.Instance.Manager.Cache.Purge(mod); + manager.Cache.Purge(mod); } } } @@ -1268,12 +1265,13 @@ private void _UpdateFilters() selected_mod = (GUIMod)ModGrid.CurrentRow.Tag; } - var inst = Main.Instance.CurrentInstance; - var registry = RegistryManager.Instance(inst, repoData).registry; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; ModGrid.Rows.Clear(); + var instName = currentInstance.Name; + var instGame = currentInstance.game; rows.AsParallel().ForAll(row => row.Visible = mainModList.IsVisible((GUIMod)row.Tag, - inst.Name, inst.game, registry)); + instName, instGame, registry)); ApplyHeaderGlyphs(); ModGrid.Rows.AddRange(Sort(rows.Where(row => row.Visible)).ToArray()); @@ -1300,12 +1298,12 @@ private bool _UpdateModsList(Dictionary old_modules = null) { log.Info("Updating the mod list"); - var regMgr = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData); + var regMgr = RegistryManager.Instance(currentInstance, repoData); IRegistryQuerier registry = regMgr.registry; repoData.Prepopulate( registry.Repositories.Values.ToList(), - new Progress(p => Main.Instance.currentUser.RaiseProgress( + new Progress(p => user.RaiseProgress( Properties.Resources.LoadingCachedRepoData, p))); if (!regMgr.registry.HasAnyAvailable()) @@ -1318,41 +1316,28 @@ private bool _UpdateModsList(Dictionary old_modules = null) regMgr.ScanUnmanagedFiles(); RaiseMessage?.Invoke(Properties.Resources.MainModListLoadingInstalled); - var versionCriteria = Main.Instance.CurrentInstance.VersionCriteria(); - - var installedIdents = registry.InstalledModules - .Select(im => im.identifier) - .ToHashSet(); - var gui_mods = registry.InstalledModules - .AsParallel() - .Where(instMod => !instMod.Module.IsDLC) - .Select(instMod => new GUIMod( - instMod, repoData, registry, versionCriteria, null, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV)) - .Concat(registry.CompatibleModules(versionCriteria) - .Where(m => !installedIdents.Contains(m.identifier)) - .AsParallel() - .Where(m => !m.IsDLC) - .Select(m => new GUIMod( - m, repoData, registry, versionCriteria, null, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV))) - .Concat(registry.IncompatibleModules(versionCriteria) - .Where(m => !installedIdents.Contains(m.identifier)) - .AsParallel() - .Where(m => !m.IsDLC) - .Select(m => new GUIMod( - m, repoData, registry, versionCriteria, true, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV))) - .ToHashSet(); + + var guiMods = mainModList.GetGUIMods(registry, repoData, currentInstance, guiConfig) + .ToHashSet(); + + foreach (var gmod in mainModList.full_list_of_mod_rows + ?.Values + .Select(row => row.Tag) + .OfType() + ?? Enumerable.Empty()) + { + gmod.PropertyChanged -= guiModule_PropertyChanged; + } + foreach (var gmod in guiMods) + { + gmod.PropertyChanged += guiModule_PropertyChanged; + } RaiseMessage?.Invoke(Properties.Resources.MainModListPreservingNew); var toNotify = new HashSet(); if (old_modules != null) { - foreach (GUIMod gm in gui_mods) + foreach (GUIMod gm in guiMods) { if (old_modules.TryGetValue(gm.Identifier, out bool oldIncompat)) { @@ -1373,23 +1358,20 @@ private bool _UpdateModsList(Dictionary old_modules = null) else { // Copy the new mod flag from the old list. - var old_new_mods = new HashSet( - mainModList.Modules.Where(m => m.IsNew)); - foreach (var gui_mod in gui_mods.Intersect(old_new_mods)) + var oldNewMods = mainModList.Modules.Where(m => m.IsNew) + .ToHashSet(); + foreach (var guiMod in guiMods.Intersect(oldNewMods)) { - gui_mod.IsNew = true; + guiMod.IsNew = true; } } LabelsAfterUpdate?.Invoke(toNotify); RaiseMessage?.Invoke(Properties.Resources.MainModListPopulatingList); // Update our mod listing - mainModList.ConstructModList(gui_mods, - Main.Instance.CurrentInstance.Name, - Main.Instance.CurrentInstance.game, - ChangeSet); + mainModList.ConstructModList(guiMods, currentInstance.Name, currentInstance.game, ChangeSet); - UpdateChangeSetAndConflicts(Main.Instance.CurrentInstance, registry); + UpdateChangeSetAndConflicts(currentInstance, registry); RaiseMessage?.Invoke(Properties.Resources.MainModListUpdatingFilters); @@ -1438,22 +1420,6 @@ private bool _UpdateModsList(Dictionary old_modules = null) return true; } - [ForbidGUICalls] - public void MarkModForInstall(string identifier, bool uncheck = false) - { - Util.Invoke(this, () => _MarkModForInstall(identifier, uncheck)); - } - - private void _MarkModForInstall(string identifier, bool uninstall) - { - DataGridViewRow row = mainModList?.full_list_of_mod_rows?[identifier]; - var mod = (GUIMod)row?.Tag; - if (mod?.Identifier == identifier) - { - mod.SetInstallChecked(row, Installed, !uninstall); - } - } - private void ModGrid_CurrentCellDirtyStateChanged(object sender, EventArgs e) { ModGrid_CellContentClick(sender, null); @@ -1663,10 +1629,10 @@ private int VersionPieceCompare(bool definedA, int valA, bool definedB, int valB ? (definedB ? valA.CompareTo(valB) : -1) : (definedB ? 1 : 0); - public void ResetFilterAndSelectModOnList(string key) + public void ResetFilterAndSelectModOnList(CkanModule module) { EditModSearches.Clear(); - FocusMod(key, true); + FocusMod(module.identifier, true); } public GUIMod SelectedModule => @@ -1689,15 +1655,14 @@ public void ParentMoved() [ForbidGUICalls] private void UpdateHiddenTagsAndLabels() { - var inst = Main.Instance.CurrentInstance; - var registry = RegistryManager.Instance(inst, repoData).registry; + var registry = RegistryManager.Instance(currentInstance, repoData).registry; var tags = mainModList.ModuleTags.HiddenTags .Intersect(registry.Tags.Keys) .OrderByDescending(tagName => tagName) .Select(tagName => registry.Tags[tagName]) .ToList(); - var labels = mainModList.ModuleLabels.LabelsFor(inst.Name) - .Where(l => l.Hide && l.ModuleCount(inst.game) > 0) + var labels = mainModList.ModuleLabels.LabelsFor(currentInstance.Name) + .Where(l => l.Hide && l.ModuleCount(currentInstance.game) > 0) .ToList(); hiddenTagsLabelsLinkList.UpdateTagsAndLabels(tags, labels); Util.Invoke(hiddenTagsLabelsLinkList, () => @@ -1852,6 +1817,12 @@ public void InstanceUpdated() ModGrid.CurrentCell = null; } + public HashSet ComputeUserChangeSet() + => mainModList.ComputeUserChangeSet( + RegistryManager.Instance(currentInstance, repoData).registry, + currentInstance.VersionCriteria(), + ReplaceCol); + [ForbidGUICalls] public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier registry) { @@ -1864,20 +1835,29 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi List full_change_set = null; Dictionary new_conflicts = null; - var user_change_set = mainModList.ComputeUserChangeSet(registry, inst.VersionCriteria()); + var gameVersion = inst.VersionCriteria(); + var user_change_set = mainModList.ComputeUserChangeSet(registry, gameVersion, ReplaceCol); try { - var gameVersion = inst.VersionCriteria(); + // Set the target versions of upgrading mods based on what's actually allowed + foreach (var ch in user_change_set.OfType()) + { + if (mainModList.full_list_of_mod_rows[ch.Mod.identifier].Tag is GUIMod gmod) + { + // This setter calls UpdateChangeSetAndConflicts, so there's a risk of + // an infinite loop here. Tread lightly! + gmod.SelectedMod = ch.targetMod; + } + } var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, gameVersion); full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( item => new GUIMod(item.Key, repoData, registry, gameVersion, null, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV), + guiConfig.HideEpochs, guiConfig.HideV), item => item.Value); if (new_conflicts.Count > 0) { - RaiseMessage?.Invoke(string.Join("; ", tuple.Item3)); + SetStatusBar?.Invoke(string.Join("; ", tuple.Item3)); } else { @@ -1889,9 +1869,11 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi { RaiseError?.Invoke(string.Format(Properties.Resources.MainDepNotSatisfied, k.parent, k.module)); - // Uncheck the box - MarkModForInstall(k.parent.identifier, true); + if (mainModList.full_list_of_mod_rows[k.parent.identifier].Tag is GUIMod gmod) + { + gmod.SelectedMod = null; + } } Conflicts = new_conflicts; diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index 3e8b4b76da..b382393a66 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -21,6 +21,7 @@ public ModInfo() { InitializeComponent(); Contents.OnDownloadClick += gmod => OnDownloadClick?.Invoke(gmod); + Relationships.ModuleDoubleClicked += mod => ModuleDoubleClicked?.Invoke(mod); } public GUIMod SelectedModule @@ -54,6 +55,7 @@ public void RefreshModContentsTree() public event Action OnDownloadClick; public event Action OnChangeFilter; + public event Action ModuleDoubleClicked; protected override void OnResize(EventArgs e) { diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index f181e05752..18ccd38142 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -27,7 +27,6 @@ public GUIMod SelectedModule { set { - var module = value?.ToModule(); if (value != selectedModule) { selectedModule = value; @@ -93,7 +92,7 @@ private void _UpdateModContentsTree(CkanModule module, bool force = false) ContentsPreviewTree.Enabled = true; ContentsPreviewTree.Nodes.Clear(); var rootNode = ContentsPreviewTree.Nodes.Add("", module.ToString(), "folderZip", "folderZip"); - if (!Main.Instance.Manager.Cache.IsMaybeCachedZip(module)) + if (!manager.Cache.IsMaybeCachedZip(module)) { NotCachedLabel.Text = Properties.Resources.ModInfoNotCached; ContentsDownloadButton.Enabled = true; @@ -103,7 +102,7 @@ private void _UpdateModContentsTree(CkanModule module, bool force = false) else { rootNode.Text = Path.GetFileName( - Main.Instance.Manager.Cache.GetCachedFilename(module)); + manager.Cache.GetCachedFilename(module)); NotCachedLabel.Text = Properties.Resources.ModInfoCached; ContentsDownloadButton.Enabled = false; ContentsOpenButton.Enabled = true; @@ -114,7 +113,7 @@ private void _UpdateModContentsTree(CkanModule module, bool force = false) { var paths = new ModuleInstaller( manager.CurrentInstance, - Main.Instance.Manager.Cache, + manager.Cache, Main.Instance.currentUser) .GetModuleContentsList(module) // Load fully in bg diff --git a/GUI/Controls/ModInfoTabs/Relationships.cs b/GUI/Controls/ModInfoTabs/Relationships.cs index 3cce1470a0..913bafcb1a 100644 --- a/GUI/Controls/ModInfoTabs/Relationships.cs +++ b/GUI/Controls/ModInfoTabs/Relationships.cs @@ -76,6 +76,8 @@ public GUIMod SelectedModule get => selectedModule; } + public event Action ModuleDoubleClicked; + private void UpdateModDependencyGraph(CkanModule module) { Util.Invoke(DependsGraphTree, () => _UpdateModDependencyGraph(module)); @@ -87,7 +89,10 @@ private void UpdateModDependencyGraph(CkanModule module) private void DependsGraphTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { - Main.Instance.ManageMods.ResetFilterAndSelectModOnList(e.Node.Name); + if (e.Node.Tag is CkanModule module) + { + ModuleDoubleClicked?.Invoke(module); + } } private bool ImMyOwnGrandpa(TreeNode node) diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index 5f3810e5e1..a372f207a6 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -64,6 +64,10 @@ public GUIMod SelectedModule } } + private GameInstance currentInstance => Main.Instance.CurrentInstance; + private GameInstanceManager manager => Main.Instance.Manager; + private IUser user => Main.Instance.currentUser; + private readonly RepositoryDataManager repoData; private GUIMod visibleGuiModule; private bool ignoreItemCheck; @@ -71,39 +75,37 @@ public GUIMod SelectedModule private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) { - if (ignoreItemCheck || e.CurrentValue == e.NewValue) - { - return; - } - ListViewItem item = VersionsListView.Items[e.Index]; - CkanModule module = item.Tag as CkanModule; - switch (e.NewValue) + if (!ignoreItemCheck && e.CurrentValue != e.NewValue + && VersionsListView.Items[e.Index].Tag is CkanModule module) { - case CheckState.Checked: - if (allowInstall(module)) - { - // Add this version to the change set - visibleGuiModule.SelectedMod = module; - } - else - { - // Abort! Abort! - e.NewValue = CheckState.Unchecked; - } - break; + switch (e.NewValue) + { + case CheckState.Checked: + if (allowInstall(module)) + { + // Add this version to the change set + visibleGuiModule.SelectedMod = module; + } + else + { + // Abort! Abort! + e.NewValue = CheckState.Unchecked; + } + break; - case CheckState.Unchecked: - // Remove or cancel installation - visibleGuiModule.SelectedMod = null; - break; + case CheckState.Unchecked: + // Remove or cancel installation + visibleGuiModule.SelectedMod = null; + break; + } } } [ForbidGUICalls] - private static bool installable(ModuleInstaller installer, - CkanModule module, - IRegistryQuerier registry) - => installable(installer, module, registry, Main.Instance.CurrentInstance.VersionCriteria()); + private bool installable(ModuleInstaller installer, + CkanModule module, + IRegistryQuerier registry) + => installable(installer, module, registry, currentInstance.VersionCriteria()); [ForbidGUICalls] private static bool installable(ModuleInstaller installer, @@ -117,12 +119,8 @@ private static bool installable(ModuleInstaller installer, private bool allowInstall(CkanModule module) { - GameInstance currentInstance = Main.Instance.Manager.CurrentInstance; IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; - var installer = new ModuleInstaller( - currentInstance, - Main.Instance.Manager.Cache, - Main.Instance.currentUser); + var installer = new ModuleInstaller(currentInstance, manager.Cache, user); return installable(installer, module, registry) || Main.Instance.YesNoDialog( @@ -157,7 +155,6 @@ private void UpdateSelection() private List getVersions(GUIMod gmod) { - GameInstance currentInstance = Main.Instance.Manager.CurrentInstance; IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; // Can't be functional because AvailableByIdentifier throws exceptions @@ -183,7 +180,6 @@ private List getVersions(GUIMod gmod) private ListViewItem[] getItems(GUIMod gmod, List versions) { - GameInstance currentInstance = Main.Instance.Manager.CurrentInstance; IRegistryQuerier registry = RegistryManager.Instance(currentInstance, repoData).registry; ModuleVersion installedVersion = registry.InstalledVersion(gmod.Identifier); @@ -220,20 +216,20 @@ private ListViewItem[] getItems(GUIMod gmod, List versions) [ForbidGUICalls] private void checkInstallable(ListViewItem[] items) { - var currentInstance = Main.Instance.Manager.CurrentInstance; - var registry = RegistryManager.Instance(currentInstance, repoData).registry; - var installer = new ModuleInstaller(currentInstance, - Main.Instance.Manager.Cache, - Main.Instance.currentUser); + var registry = RegistryManager.Instance(currentInstance, repoData).registry; + var installer = new ModuleInstaller(currentInstance, manager.Cache, user); ListViewItem latestCompatible = null; // Load balance the items so they're processed roughly in-order instead of blocks Partitioner.Create(items, true) // Distribute across cores .AsParallel() - // Abort when they switch to another mod - .WithCancellation(cancelTokenSrc.Token) // Return them as they're processed .WithMergeOptions(ParallelMergeOptions.NotBuffered) + // Abort when they switch to another mod + .WithCancellation(cancelTokenSrc.Token) + // Check the important ones first + .OrderBy(item => (item.Tag as CkanModule) != visibleGuiModule.InstalledMod?.Module + && (item.Tag as CkanModule) != visibleGuiModule.SelectedMod) // Slow step to be performed across multiple cores .Where(item => installable(installer, item.Tag as CkanModule, registry)) // Jump back to GUI thread for the updates for each compatible item diff --git a/GUI/Dialogs/ErrorDialog.cs b/GUI/Dialogs/ErrorDialog.cs index 60155e8b33..123fc67aa8 100644 --- a/GUI/Dialogs/ErrorDialog.cs +++ b/GUI/Dialogs/ErrorDialog.cs @@ -22,9 +22,9 @@ public ErrorDialog() } [ForbidGUICalls] - public void ShowErrorDialog(string text, params object[] args) + public void ShowErrorDialog(Main mainForm, string text, params object[] args) { - Util.Invoke(Main.Instance, () => + Util.Invoke(this, () => { log.ErrorFormat(text, args); // Append to previous text, if any @@ -45,19 +45,14 @@ public void ShowErrorDialog(string text, params object[] args) ErrorMessage.Width - 4))); if (!Visible) { - StartPosition = Main.Instance.actuallyVisible + StartPosition = mainForm.actuallyVisible ? FormStartPosition.CenterParent : FormStartPosition.CenterScreen; - ShowDialog(Main.Instance); + ShowDialog(mainForm); } }); } - public void HideErrorDialog() - { - Util.Invoke(this, () => Close()); - } - private void DismissButton_Click(object sender, EventArgs e) { Close(); diff --git a/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs b/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs index 98d4efcebb..c544559e77 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs +++ b/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs @@ -250,7 +250,7 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader GamePlayTime; private System.Windows.Forms.ColumnHeader GameInstallPath; private System.Windows.Forms.Button SelectButton; - private DropdownMenuButton AddNewButton; + private CKAN.GUI.DropdownMenuButton AddNewButton; private System.Windows.Forms.ContextMenuStrip AddNewMenu; private System.Windows.Forms.ContextMenuStrip InstanceListContextMenuStrip; private System.Windows.Forms.ToolStripMenuItem openDirectoryMenuItem; diff --git a/GUI/Dialogs/ManageGameInstancesDialog.cs b/GUI/Dialogs/ManageGameInstancesDialog.cs index 6ab1a9a74c..f356534824 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.cs +++ b/GUI/Dialogs/ManageGameInstancesDialog.cs @@ -18,7 +18,7 @@ namespace CKAN.GUI #endif public partial class ManageGameInstancesDialog : Form { - private readonly GameInstanceManager _manager = Main.Instance.Manager; + private static GameInstanceManager manager => Main.Instance.Manager; private readonly IUser _user; private RenameInstanceDialog _renameInstanceDialog; private readonly OpenFileDialog _instanceDialog = new OpenFileDialog() @@ -27,7 +27,7 @@ public partial class ManageGameInstancesDialog : Form CheckFileExists = false, CheckPathExists = false, InitialDirectory = Environment.CurrentDirectory, - Filter = GameFolderFilter(Main.Instance.Manager), + Filter = GameFolderFilter(manager), Multiselect = false }; @@ -59,9 +59,9 @@ public ManageGameInstancesDialog(bool centerScreen, IUser user) StartPosition = FormStartPosition.CenterScreen; } - if (!_manager.Instances.Any()) + if (!manager.Instances.Any()) { - _manager.FindAndRegisterDefaultInstances(); + manager.FindAndRegisterDefaultInstances(); } // Set the renderer for the AddNewMenu @@ -80,14 +80,14 @@ public void UpdateInstancesList() GameInstancesListView.Items.Clear(); UpdateButtonState(); - var allSameGame = _manager.Instances.Select(i => i.Value.game).Distinct().Count() <= 1; - var hasPlayTime = _manager.Instances.Any(instance => (instance.Value.playTime?.Time ?? TimeSpan.Zero) > TimeSpan.Zero); + var allSameGame = manager.Instances.Select(i => i.Value.game).Distinct().Count() <= 1; + var hasPlayTime = manager.Instances.Any(instance => (instance.Value.playTime?.Time ?? TimeSpan.Zero) > TimeSpan.Zero); AddOrRemoveColumn(GameInstancesListView, Game, !allSameGame, GameInstallVersion.Index); AddOrRemoveColumn(GameInstancesListView, GamePlayTime, hasPlayTime, GameInstallPath.Index); GameInstancesListView.Items.AddRange( - _manager.Instances.OrderByDescending(instance => instance.Value.game.FirstReleaseDate) + manager.Instances.OrderByDescending(instance => instance.Value.game.FirstReleaseDate) .ThenByDescending(instance => instance.Value.Version()) .ThenBy(instance => instance.Key) .Select(instance => new ListViewItem( @@ -119,7 +119,7 @@ private string[] rowItems(GameInstance instance, bool includeGame, bool includeP { !instance.Valid ? string.Format(Properties.Resources.ManageGameInstancesNameColumnInvalid, instance.Name) - : !(_manager.CurrentInstance?.Equals(instance) ?? false) && instance.IsMaybeLocked + : !(manager.CurrentInstance?.Equals(instance) ?? false) && instance.IsMaybeLocked ? string.Format(Properties.Resources.ManageGameInstancesNameColumnLocked, instance.Name) : instance.Name }; @@ -180,8 +180,8 @@ private void AddToCKANMenuItem_Click(object sender, EventArgs e) { instanceName = path; } - instanceName = _manager.GetNextValidInstanceName(instanceName); - _manager.AddInstance(path, instanceName, _user); + instanceName = manager.GetNextValidInstanceName(instanceName); + manager.AddInstance(path, instanceName, _user); UpdateInstancesList(); } catch (NotKSPDirKraken k) @@ -197,24 +197,24 @@ private void AddToCKANMenuItem_Click(object sender, EventArgs e) private void ImportFromSteamMenuItem_Click(object sender, EventArgs e) { - var currentDirs = _manager.Instances.Values + var currentDirs = manager.Instances.Values .Select(inst => inst.GameDir()) .ToHashSet(Platform.PathComparer); - var toAdd = _manager.FindDefaultInstances() - .Where(inst => !currentDirs.Contains(inst.GameDir())); + var toAdd = manager.FindDefaultInstances() + .Where(inst => !currentDirs.Contains(inst.GameDir())); foreach (var inst in toAdd) { - _manager.AddInstance(inst); + manager.AddInstance(inst); } UpdateInstancesList(); } private void CloneGameInstanceMenuItem_Click(object sender, EventArgs e) { - var old_instance = Main.Instance.CurrentInstance; + var old_instance = manager.CurrentInstance; - var result = new CloneGameInstanceDialog(_manager, _user, (string)GameInstancesListView.SelectedItems[0].Tag).ShowDialog(this); - if (result == DialogResult.OK && !Equals(old_instance, Main.Instance.CurrentInstance)) + var result = new CloneGameInstanceDialog(manager, _user, (string)GameInstancesListView.SelectedItems[0].Tag).ShowDialog(this); + if (result == DialogResult.OK && !Equals(old_instance, manager.CurrentInstance)) { DialogResult = DialogResult.OK; Close(); @@ -240,7 +240,7 @@ private void UseSelectedInstance() { try { - _manager.SetCurrentInstance(instName); + manager.SetCurrentInstance(instName); DialogResult = DialogResult.OK; Close(); } @@ -255,7 +255,7 @@ private void SetAsDefaultCheckbox_Click(object sender, EventArgs e) { if (SetAsDefaultCheckbox.Checked) { - _manager.ClearAutoStart(); + manager.ClearAutoStart(); SetAsDefaultCheckbox.Checked = false; return; } @@ -265,7 +265,7 @@ private void SetAsDefaultCheckbox_Click(object sender, EventArgs e) { try { - _manager.SetAutoStart(instName); + manager.SetAutoStart(instName); SetAsDefaultCheckbox.Checked = true; } catch (NotKSPDirKraken k) @@ -285,7 +285,7 @@ private void GameInstancesListView_SelectedIndexChanged(object sender, EventArgs } string instName = (string)GameInstancesListView.SelectedItems[0].Tag; - SetAsDefaultCheckbox.Checked = _manager.AutoStartInstance?.Equals(instName) ?? false; + SetAsDefaultCheckbox.Checked = manager.AutoStartInstance?.Equals(instName) ?? false; } private void GameInstancesListView_DoubleClick(object sender, EventArgs r) @@ -317,7 +317,7 @@ private void GameInstancesListView_KeyDown(object sender, KeyEventArgs e) private void OpenDirectoryMenuItem_Click(object sender, EventArgs e) { - string path = _manager.Instances[(string) GameInstancesListView.SelectedItems[0].Tag].GameDir(); + string path = manager.Instances[(string) GameInstancesListView.SelectedItems[0].Tag].GameDir(); if (!Directory.Exists(path)) { @@ -340,7 +340,7 @@ private void RenameButton_Click(object sender, EventArgs e) } // proceed with instance rename - _manager.RenameInstance(instance, _renameInstanceDialog.GetResult()); + manager.RenameInstance(instance, _renameInstanceDialog.GetResult()); UpdateInstancesList(); } @@ -348,7 +348,7 @@ private void Forget_Click(object sender, EventArgs e) { foreach (var instance in GameInstancesListView.SelectedItems.OfType().Select(item => item.Tag as string)) { - _manager.RemoveInstance(instance); + manager.RemoveInstance(instance); UpdateInstancesList(); } } @@ -356,8 +356,8 @@ private void Forget_Click(object sender, EventArgs e) private void UpdateButtonState() { RenameButton.Enabled = SelectButton.Enabled = SetAsDefaultCheckbox.Enabled = CloneGameInstanceMenuItem.Enabled = HasSelections; - ForgetButton.Enabled = HasSelections && (string)GameInstancesListView.SelectedItems[0].Tag != _manager.CurrentInstance?.Name; - ImportFromSteamMenuItem.Enabled = _manager.SteamLibrary.Games.Length > 0; + ForgetButton.Enabled = HasSelections && (string)GameInstancesListView.SelectedItems[0].Tag != manager.CurrentInstance?.Name; + ImportFromSteamMenuItem.Enabled = manager.SteamLibrary.Games.Length > 0; } } } diff --git a/GUI/Dialogs/PluginsDialog.cs b/GUI/Dialogs/PluginsDialog.cs index 4e19a3a634..1f32088654 100644 --- a/GUI/Dialogs/PluginsDialog.cs +++ b/GUI/Dialogs/PluginsDialog.cs @@ -17,6 +17,8 @@ public PluginsDialog() StartPosition = FormStartPosition.CenterScreen; } + private PluginController pluginController => Main.Instance.pluginController; + private readonly OpenFileDialog m_AddNewPluginDialog = new OpenFileDialog(); private void PluginsDialog_Load(object sender, EventArgs e) @@ -35,7 +37,7 @@ private void PluginsDialog_Load(object sender, EventArgs e) private void RefreshActivePlugins() { - var activePlugins = Main.Instance.pluginController.ActivePlugins; + var activePlugins = pluginController.ActivePlugins; ActivePluginsListBox.Items.Clear(); foreach (var plugin in activePlugins) @@ -46,7 +48,7 @@ private void RefreshActivePlugins() private void RefreshDormantPlugins() { - var dormantPlugins = Main.Instance.pluginController.DormantPlugins; + var dormantPlugins = pluginController.DormantPlugins; DormantPluginsListBox.Items.Clear(); foreach (var plugin in dormantPlugins) @@ -70,7 +72,7 @@ private void DeactivateButton_Click(object sender, EventArgs e) } var plugin = (IGUIPlugin) ActivePluginsListBox.SelectedItem; - Main.Instance.pluginController.DeactivatePlugin(plugin); + pluginController.DeactivatePlugin(plugin); RefreshActivePlugins(); RefreshDormantPlugins(); } @@ -83,8 +85,8 @@ private void ReloadPluginButton_Click(object sender, EventArgs e) } var plugin = (IGUIPlugin)ActivePluginsListBox.SelectedItem; - Main.Instance.pluginController.DeactivatePlugin(plugin); - Main.Instance.pluginController.ActivatePlugin(plugin); + pluginController.DeactivatePlugin(plugin); + pluginController.ActivatePlugin(plugin); RefreshActivePlugins(); RefreshDormantPlugins(); } @@ -104,7 +106,7 @@ private void ActivatePluginButton_Click(object sender, EventArgs e) } var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; - Main.Instance.pluginController.ActivatePlugin(plugin); + pluginController.ActivatePlugin(plugin); RefreshActivePlugins(); RefreshDormantPlugins(); } @@ -117,7 +119,7 @@ private void UnloadPluginButton_Click(object sender, EventArgs e) } var plugin = (IGUIPlugin)DormantPluginsListBox.SelectedItem; - Main.Instance.pluginController.UnloadPlugin(plugin); + pluginController.UnloadPlugin(plugin); RefreshActivePlugins(); RefreshDormantPlugins(); } @@ -127,7 +129,7 @@ private void AddNewPluginButton_Click(object sender, EventArgs e) if (m_AddNewPluginDialog.ShowDialog(this) == DialogResult.OK) { var path = m_AddNewPluginDialog.FileName; - Main.Instance.pluginController.AddNewAssemblyToPluginsPath(path); + pluginController.AddNewAssemblyToPluginsPath(path); RefreshDormantPlugins(); } } diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index fb78d80d8b..5cadd1215d 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -25,6 +25,8 @@ public partial class SettingsDialog : Form public bool RepositoryRemoved { get; private set; } = false; public bool RepositoryMoved { get; private set; } = false; + private GameInstanceManager manager => Main.Instance.Manager; + private readonly IConfiguration coreConfig; private readonly GUIConfiguration guiConfig; private readonly RegistryManager regMgr; @@ -103,7 +105,7 @@ private void UpdateAutoUpdate() protected override void OnFormClosing(FormClosingEventArgs e) { if (CachePath.Text != coreConfig.DownloadCacheDir - && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + && !manager.TrySetupCache(CachePath.Text, out string failReason)) { user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); e.Cancel = true; @@ -260,13 +262,13 @@ private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) { // Switch main cache since user seems committed to this path if (CachePath.Text != coreConfig.DownloadCacheDir - && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + && !manager.TrySetupCache(CachePath.Text, out string failReason)) { user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); return; } - Main.Instance.Manager.Cache.EnforceSizeLimit( + manager.Cache.EnforceSizeLimit( coreConfig.CacheSizeLimit.Value, regMgr.registry); UpdateCacheInfo(coreConfig.DownloadCacheDir); @@ -277,13 +279,13 @@ private void PurgeAllMenuItem_Click(object sender, EventArgs e) { // Switch main cache since user seems committed to this path if (CachePath.Text != coreConfig.DownloadCacheDir - && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) + && !manager.TrySetupCache(CachePath.Text, out string failReason)) { user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); return; } - Main.Instance.Manager.Cache.GetSizeInfo( + manager.Cache.GetSizeInfo( out int cacheFileCount, out long cacheSize, out _); YesNoDialog deleteConfirmationDialog = new YesNoDialog(); @@ -295,7 +297,7 @@ private void PurgeAllMenuItem_Click(object sender, EventArgs e) if (deleteConfirmationDialog.ShowYesNoDialog(this, confirmationText) == DialogResult.Yes) { // Tell the cache object to nuke itself - Main.Instance.Manager.Cache.RemoveAll(); + manager.Cache.RemoveAll(); UpdateCacheInfo(coreConfig.DownloadCacheDir); } diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs index b99cd71b2b..2ba6a4d220 100644 --- a/GUI/Labels/ModuleLabel.cs +++ b/GUI/Labels/ModuleLabel.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.ComponentModel; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; @@ -80,6 +81,11 @@ public bool ContainsModule(IGame game, string identifier) public bool AppliesTo(string instanceName) => InstanceName == null || InstanceName == instanceName; + public IEnumerable IdentifiersFor(IGame game) + => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet idents) + ? idents + : Enumerable.Empty(); + /// /// Add a module to this label's group /// diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs index 279e6cac6c..fb3f069d37 100644 --- a/GUI/Labels/ModuleLabelList.cs +++ b/GUI/Labels/ModuleLabelList.cs @@ -79,5 +79,10 @@ public bool Save(string path) return false; } } + + public IEnumerable HeldIdentifiers(GameInstance inst) + => LabelsFor(inst.Name).Where(l => l.HoldVersion) + .SelectMany(l => l.IdentifiersFor(inst.game)) + .Distinct(); } } diff --git a/GUI/Main/Main.Designer.cs b/GUI/Main/Main.Designer.cs index f88f7ba4e5..751af526f9 100644 --- a/GUI/Main/Main.Designer.cs +++ b/GUI/Main/Main.Designer.cs @@ -482,6 +482,7 @@ private void InitializeComponent() this.ManageMods.StartChangeSet += this.ManageMods_StartChangeSet; this.ManageMods.RaiseMessage += this.ManageMods_RaiseMessage; this.ManageMods.RaiseError += this.ManageMods_RaiseError; + this.ManageMods.SetStatusBar += this.ManageMods_SetStatusBar; this.ManageMods.ClearStatusBar += this.ManageMods_ClearStatusBar; this.ManageMods.LaunchGame += this.LaunchGame; this.ManageMods.EditCommandLines += this.EditCommandLines; diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 832c112ec3..87bda9f2f0 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -101,6 +101,7 @@ public Main(string[] cmdlineArgs, GameInstanceManager mgr) InitializeComponent(); // React when the user clicks a tag or filter link in mod info ModInfo.OnChangeFilter += ManageMods.Filter; + ModInfo.ModuleDoubleClicked += ManageMods.ResetFilterAndSelectModOnList; repoData = ServiceLocator.Container.Resolve(); Instance = this; @@ -256,7 +257,8 @@ protected override void OnShown(EventArgs e) if (!string.IsNullOrEmpty(regMgr.previousCorruptedMessage) && !string.IsNullOrEmpty(regMgr.previousCorruptedPath)) { - errorDialog.ShowErrorDialog(Properties.Resources.MainCorruptedRegistry, + errorDialog.ShowErrorDialog(this, + Properties.Resources.MainCorruptedRegistry, regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage, Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename())); regMgr.previousCorruptedMessage = null; @@ -405,7 +407,8 @@ private void CurrentInstanceUpdated() if (!string.IsNullOrEmpty(regMgr.previousCorruptedMessage) && !string.IsNullOrEmpty(regMgr.previousCorruptedPath)) { - errorDialog.ShowErrorDialog(Properties.Resources.MainCorruptedRegistry, + errorDialog.ShowErrorDialog(this, + Properties.Resources.MainCorruptedRegistry, regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage, Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename())); regMgr.previousCorruptedMessage = null; @@ -443,7 +446,6 @@ private void CurrentInstanceUpdated() // If not allowing, don't do anything if (repoUpdateNeeded) { - ManageMods.ModGrid.Rows.Clear(); UpdateRepo(refreshWithoutChanges: true); } else @@ -876,6 +878,11 @@ private void ManageMods_RaiseError(string error) currentUser.RaiseError(error); } + private void ManageMods_SetStatusBar(string message) + { + StatusLabel.ToolTipText = StatusLabel.Text = message; + } + private void ManageMods_ClearStatusBar() { StatusLabel.ToolTipText = StatusLabel.Text = ""; diff --git a/GUI/Main/MainDialogs.cs b/GUI/Main/MainDialogs.cs index fddcd3d316..da88273406 100644 --- a/GUI/Main/MainDialogs.cs +++ b/GUI/Main/MainDialogs.cs @@ -28,7 +28,7 @@ public void RecreateDialogs() [ForbidGUICalls] public void ErrorDialog(string text, params object[] args) { - errorDialog.ShowErrorDialog(text, args); + errorDialog.ShowErrorDialog(this, text, args); } [ForbidGUICalls] diff --git a/GUI/Main/MainImport.cs b/GUI/Main/MainImport.cs index b59ae5abbc..04c515e7a3 100644 --- a/GUI/Main/MainImport.cs +++ b/GUI/Main/MainImport.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using System.Windows.Forms; +using System.Linq; // Don't warn if we use our own obsolete properties #pragma warning disable 0618 @@ -42,9 +43,18 @@ private void ImportModules() new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser).ImportFiles( GetFiles(dlg.FileNames), currentUser, - (CkanModule mod) => ManageMods.MarkModForInstall(mod.identifier, false), - RegistryManager.Instance(CurrentInstance, repoData).registry - ); + (CkanModule mod) => + { + if (ManageMods.mainModList + .full_list_of_mod_rows + .TryGetValue(mod.identifier, + out DataGridViewRow row) + && row.Tag is GUIMod gmod) + { + gmod.SelectedMod = mod; + } + }, + RegistryManager.Instance(CurrentInstance, repoData).registry); } finally { @@ -56,14 +66,8 @@ private void ImportModules() } private HashSet GetFiles(string[] filenames) - { - HashSet files = new HashSet(); - foreach (string fn in filenames) - { - files.Add(new FileInfo(fn)); - } - return files; - } + => filenames.Select(fn => new FileInfo(fn)) + .ToHashSet(); private static readonly string[] downloadPaths = new string[] { diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index a97cbab168..6f6073347e 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -146,10 +146,7 @@ private void minimizeNotifyIcon_BalloonTipClicked(object sender, EventArgs e) // Install Wait.StartWaiting(InstallMods, PostInstallMods, true, new InstallArgument( - ManageMods.mainModList - .ComputeUserChangeSet( - RegistryManager.Instance(CurrentInstance, repoData).registry, - CurrentInstance.VersionCriteria()) + ManageMods.ComputeUserChangeSet() .ToList(), RelationshipResolverOptions.DependsOnlyOpts()) ); diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index 37366a87dc..80fff23cc7 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -24,12 +24,15 @@ public sealed class GUIMod : INotifyPropertyChanged public CkanModule LatestAvailableMod { get; private set; } public InstalledModule InstalledMod { get; private set; } + private GameInstance currentInstance => Main.Instance?.CurrentInstance; + private GameInstanceManager manager => Main.Instance?.Manager; + /// /// The module of the checkbox that is checked in the MainAllModVersions list if any, /// null otherwise. /// Used for generating this mod's part of the change set. /// - public CkanModule SelectedMod + public CkanModule SelectedMod { get => selectedMod; set @@ -37,25 +40,6 @@ public CkanModule SelectedMod if (!(selectedMod?.Equals(value) ?? value?.Equals(selectedMod) ?? true)) { selectedMod = value; - - if (IsInstalled && HasUpdate) - { - var isLatest = (LatestCompatibleMod?.Equals(selectedMod) ?? false); - if (IsUpgradeChecked ^ isLatest) - { - // Try upgrading if they pick the latest - Main.Instance.ManageMods.MarkModForUpdate(Identifier, isLatest); - } - - } - Main.Instance.ManageMods.MarkModForInstall(Identifier, selectedMod == null); - - var inst = Main.Instance.CurrentInstance; - Main.Instance.ManageMods.UpdateChangeSetAndConflicts( - inst, - RegistryManager.Instance(inst, - ServiceLocator.Container.Resolve()).registry); - OnPropertyChanged(); } } @@ -75,7 +59,7 @@ private void OnPropertyChanged([CallerMemberName] string name = null) public string Name { get; private set; } public bool IsInstalled { get; private set; } public bool IsAutoInstalled { get; private set; } - public bool HasUpdate { get; private set; } + public bool HasUpdate { get; set; } public bool HasReplacement { get; private set; } public bool IsIncompatible { get; private set; } public bool IsAutodetected { get; private set; } @@ -100,9 +84,6 @@ public string GameCompatibility public string Abstract { get; private set; } public string Description { get; private set; } public string Identifier { get; private set; } - public bool IsInstallChecked { get; set; } - public bool IsUpgradeChecked { get; private set; } - public bool IsReplaceChecked { get; private set; } public bool IsNew { get; set; } public bool IsCKAN => Mod != null; public string Abbrevation { get; private set; } @@ -146,7 +127,6 @@ public GUIMod(InstalledModule instMod, : this(instMod.Module, repoDataMgr, registry, current_game_version, incompatible, hideEpochs, hideV) { IsInstalled = true; - IsInstallChecked = true; InstalledMod = instMod; selectedMod = registry.GetModuleByVersion(instMod.identifier, instMod.Module.version) ?? instMod.Module; @@ -185,7 +165,6 @@ public GUIMod(CkanModule mod, Description = mod.description?.Trim() ?? string.Empty; Abbrevation = new string(Name.Split(' ').Where(s => s.Length > 0).Select(s => s[0]).ToArray()); - HasUpdate = registry.HasUpdate(mod.identifier, current_game_version); HasReplacement = registry.GetReplacement(mod, current_game_version) != null; DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); InstallSize = mod.install_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.install_size); @@ -204,7 +183,7 @@ public GUIMod(CkanModule mod, if (GameCompatibilityVersion.IsAny) { GameCompatibilityVersion = mod.LatestCompatibleRealGameVersion( - Main.Instance?.Manager.CurrentInstance?.game.KnownVersions + currentInstance?.game.KnownVersions ?? new List() {}); } } @@ -269,7 +248,7 @@ private GUIMod(string identifier, if (LatestAvailableMod != null) { GameCompatibilityVersion = registry.LatestCompatibleGameVersion( - Main.Instance?.Manager.CurrentInstance?.game.KnownVersions ?? new List() {}, + currentInstance?.game.KnownVersions ?? new List() {}, identifier); } @@ -294,12 +273,12 @@ private GUIMod(string identifier, /// public void UpdateIsCached() { - if (Main.Instance?.Manager?.Cache == null || Mod?.download == null) + if (manager?.Cache == null || Mod?.download == null) { return; } - IsCached = Main.Instance.Manager.Cache.IsMaybeCachedZip(Mod); + IsCached = manager.Cache.IsMaybeCachedZip(Mod); } /// @@ -325,124 +304,34 @@ public CkanModule ToCkanModule() /// The CkanModule associated with this GUIMod or null if there is none public CkanModule ToModule() => Mod; - public IEnumerable GetModChanges() + public IEnumerable GetModChanges(bool replaceChecked) { - bool selectedIsInstalled = SelectedMod?.Equals(InstalledMod?.Module) - ?? InstalledMod?.Module.Equals(SelectedMod) - // Both null - ?? true; - if (IsInstalled && IsInstallChecked && HasUpdate && IsUpgradeChecked) - { - yield return new ModUpgrade(Mod, - GUIModChangeType.Update, - SelectedMod, - false); - } - else if (IsReplaceChecked) + if (replaceChecked) { yield return new ModChange(Mod, GUIModChangeType.Replace); } - else if (!selectedIsInstalled) + else if (!(SelectedMod?.Equals(InstalledMod?.Module) + ?? InstalledMod?.Module.Equals(SelectedMod) + // Both null + ?? true)) { - if (InstalledMod != null) - { - yield return new ModChange(InstalledMod.Module, GUIModChangeType.Remove); - } - if (SelectedMod != null) + if (InstalledMod != null && SelectedMod == LatestAvailableMod) { - yield return new ModChange(SelectedMod, GUIModChangeType.Install); + yield return new ModUpgrade(Mod, + GUIModChangeType.Update, + SelectedMod, + false); } - } - } - - /// - /// Set the properties to match a change set element. - /// Doesn't update grid, use SetInstallChecked or SetUpgradeChecked - /// if you need to update the grid. - /// - /// Type of change - public void SetRequestedChange(GUIModChangeType change) - { - switch (change) - { - case GUIModChangeType.Install: - IsInstallChecked = true; - IsUpgradeChecked = false; - break; - case GUIModChangeType.Remove: - IsInstallChecked = false; - IsUpgradeChecked = false; - IsReplaceChecked = false; - break; - case GUIModChangeType.Update: - IsInstallChecked = true; - IsUpgradeChecked = true; - break; - case GUIModChangeType.Replace: - IsInstallChecked = true; - IsReplaceChecked = true; - break; - } - } - - public void SetUpgradeChecked(DataGridViewRow row, DataGridViewColumn col, bool? set_value_to = null) - { - if (row?.Cells[col.Index] is DataGridViewCheckBoxCell update_cell) - { - var old_value = (bool) update_cell.Value; - - bool value = set_value_to ?? old_value; - IsUpgradeChecked = value; - if (old_value != value) + else { - update_cell.Value = value; - SelectedMod = value ? LatestCompatibleMod - : IsAutodetected ? null - : (SelectedMod ?? InstalledMod?.Module); - } - else if (!set_value_to.HasValue) - { - var isLatest = (LatestCompatibleMod?.Equals(selectedMod) ?? false); - SelectedMod = value ? LatestCompatibleMod - : isLatest ? InstalledMod?.Module - : SelectedMod; - } - } - } - - public void SetInstallChecked(DataGridViewRow row, DataGridViewColumn col, bool? set_value_to = null) - { - if (row?.Cells[col.Index] is DataGridViewCheckBoxCell install_cell) - { - bool changeTo = set_value_to ?? (bool)install_cell.Value; - if (IsInstallChecked != changeTo) - { - IsInstallChecked = changeTo; - } - SelectedMod = changeTo ? (SelectedMod ?? Mod) : null; - // Setting this property causes ModGrid_CellValueChanged to be called, - // which calls SetInstallChecked again. Treat it conservatively. - if ((bool)install_cell.Value != IsInstallChecked) - { - install_cell.Value = IsInstallChecked; - // This call is needed to force the UI to update, - // otherwise the checkbox will look checked when it's unchecked or vice versa - row.DataGridView?.RefreshEdit(); - } - } - } - - public void SetReplaceChecked(DataGridViewRow row, DataGridViewColumn col, bool? set_value_to = null) - { - if (row.Cells[col.Index] is DataGridViewCheckBoxCell replace_cell) - { - var old_value = (bool) replace_cell.Value; - - bool value = set_value_to ?? old_value; - IsReplaceChecked = value; - if (old_value != value) - { - replace_cell.Value = value; + if (InstalledMod != null) + { + yield return new ModChange(InstalledMod.Module, GUIModChangeType.Remove); + } + if (SelectedMod != null) + { + yield return new ModChange(SelectedMod, GUIModChangeType.Install); + } } } } diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 6149de4d1e..0db9904c94 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -157,7 +157,7 @@ public override string Description /// /// The target version for upgrading /// - public readonly CkanModule targetMod; + public CkanModule targetMod; private bool IsReinstall => targetMod.identifier == Mod.identifier diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 8d605dc3bd..4e8d3aa6a7 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -422,10 +422,52 @@ public string StripEpoch(string version) private static readonly Regex ContainsEpoch = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled); private static readonly Regex RemoveEpoch = new Regex(@"^([^:]+):([^:]+)$", RegexOptions.Compiled); - public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVersionCriteria crit) + private IEnumerable rowChanges(DataGridViewRow row, DataGridViewColumn replaceCol) + => (row.Tag as GUIMod).GetModChanges( + replaceCol != null && replaceCol.Visible + && row.Cells[replaceCol.Index] is DataGridViewCheckBoxCell replaceCell + && (bool)replaceCell.Value); + + public HashSet ComputeUserChangeSet(IRegistryQuerier registry, + GameVersionCriteria crit, + DataGridViewColumn replaceCol) { log.Debug("Computing user changeset"); - var modChanges = Modules.SelectMany(mod => mod.GetModChanges()); + var modChanges = full_list_of_mod_rows?.Values + .SelectMany(row => rowChanges(row, replaceCol)) + .ToList() + ?? new List(); + + // Inter-mod dependencies can block some upgrades, which can sometimes but not always + // be overcome by upgrading both mods. Try to pick the right target versions. + if (registry != null) + { + var upgrades = modChanges.OfType() + .ToArray(); + if (upgrades.Length > 0) + { + var upgradeable = registry.CheckUpgradeable(crit, + // Hold identifiers not chosen for upgrading + registry.Installed(false) + .Select(kvp => kvp.Key) + .Except(upgrades.Select(ch => ch.Mod.identifier)) + .ToHashSet()) + [true] + .ToDictionary(m => m.identifier, + m => m); + foreach (var change in upgrades) + { + change.targetMod = upgradeable.TryGetValue(change.Mod.identifier, + out CkanModule allowedMod) + // Upgrade to the version the registry says we should + ? allowedMod + // Not upgradeable! + : change.Mod; + } + modChanges.RemoveAll(ch => ch is ModUpgrade upg && upg.Mod == upg.targetMod); + } + } + return (registry == null ? modChanges : modChanges.Union( @@ -436,6 +478,97 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVe .ToHashSet(); } + /// + /// Check upgradeability of all rows and set GUIMod.HasUpdate appropriately + /// + /// Current game instance + /// Current instance's registry + /// Currently pending changeset + /// The grid rows in case we need to replace some + /// true if any mod can be updated, false otherwise + public bool ResetHasUpdate(GameInstance inst, + IRegistryQuerier registry, + List ChangeSet, + DataGridViewRowCollection rows) + { + var upgGroups = registry.CheckUpgradeable(inst.VersionCriteria(), + ModuleLabels.HeldIdentifiers(inst) + .ToHashSet()); + foreach ((var upgradeable, var mods) in upgGroups) + { + foreach (var ident in mods.Select(m => m.identifier)) + { + var row = full_list_of_mod_rows[ident]; + if (row.Tag is GUIMod gmod && gmod.HasUpdate != upgradeable) + { + gmod.HasUpdate = upgradeable; + if (row.Visible) + { + // Swap whether the row has an upgrade checkbox + var newRow = + full_list_of_mod_rows[ident] = + MakeRow(gmod, ChangeSet, inst.Name, inst.game); + var rowIndex = row.Index; + rows.Remove(row); + rows.Insert(rowIndex, newRow); + } + } + } + } + return upgGroups[true].Count > 0; + } + + /// + /// Get all the GUI mods for the given instance. + /// + /// Registry of the instance + /// Repo data of the instance + /// Game instance + /// GUI config to use + /// Sequence of GUIMods + public IEnumerable GetGUIMods(IRegistryQuerier registry, + RepositoryDataManager repoData, + GameInstance inst, + GUIConfiguration config) + => GetGUIMods(registry, repoData, inst, inst.VersionCriteria(), + registry.InstalledModules.Select(im => im.identifier) + .ToHashSet(), + config.HideEpochs, config.HideV); + + private IEnumerable GetGUIMods(IRegistryQuerier registry, + RepositoryDataManager repoData, + GameInstance inst, + GameVersionCriteria versionCriteria, + HashSet installedIdents, + bool hideEpochs, + bool hideV) + => registry.CheckUpgradeable(versionCriteria, + ModuleLabels.HeldIdentifiers(inst) + .ToHashSet()) + .SelectMany(kvp => kvp.Value + .Where(mod => !registry.IsAutodetected(mod.identifier)) + .Select(mod => new GUIMod(registry.InstalledModule(mod.identifier), + repoData, registry, + versionCriteria, null, + hideEpochs, hideV) + { + HasUpdate = kvp.Key, + })) + .Concat(registry.CompatibleModules(versionCriteria) + .Where(m => !installedIdents.Contains(m.identifier)) + .AsParallel() + .Where(m => !m.IsDLC) + .Select(m => new GUIMod(m, repoData, registry, + versionCriteria, null, + hideEpochs, hideV))) + .Concat(registry.IncompatibleModules(versionCriteria) + .Where(m => !installedIdents.Contains(m.identifier)) + .AsParallel() + .Where(m => !m.IsDLC) + .Select(m => new GUIMod(m, repoData, registry, + versionCriteria, true, + hideEpochs, hideV))); + private static readonly ILog log = LogManager.GetLogger(typeof(ModList)); } } diff --git a/Tests/CmdLine/UpgradeTests.cs b/Tests/CmdLine/UpgradeTests.cs index a0da76a277..7e5b8b2926 100644 --- a/Tests/CmdLine/UpgradeTests.cs +++ b/Tests/CmdLine/UpgradeTests.cs @@ -100,5 +100,538 @@ public void RunCommand_IdentifierEqualsVersionSyntax_UpgradesToCorrectVersion( }); } } + + [Test, + TestCase("No mods, do nothing without crashing", + new string[] { }, + new string[] { }, + new string[] { }, + new string[] { }), + TestCase("No mods, do nothing (--all) without crashing", + new string[] { }, + new string[] { }, + null, + new string[] { }), + TestCase("Enforce version requirements of identifier=version specified mod", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.2.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { "Depender=1.1.0", "Dependency" }, + new string[] { "1.1.0", "1.1.0" }), + // Installed and latest version of the depender has a version specific depends, + // the current installed dependency is old, and we upgrade to an intermediate version + // instead of the absolute latest + // (lamont-granquist's use case with identifiers) + TestCase("Should upgrade-with-identifiers to intermediate version when installed dependency blocks latest", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { "Dependency" }, + new string[] { "1.0.0", "1.1.0" }), + // Installed and latest version of the depender has a version specific depends, + // the current installed dependency is old, and we upgrade to an intermediate version + // instead of the absolute latest + // (lamont-granquist's use case with --all) + TestCase("Should upgrade-all to intermediate version when installed dependency blocks latest", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + null, + new string[] { "1.0.0", "1.1.0" }), + // Depender stops any upgrades at all (with identifiers) + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Depender stops any upgrades-with-identifiers at all", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { "Dependency" }, + new string[] { "1.0.0", "1.0.0" }), + // Depender stops any upgrades at all (--all) + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Depender stops any upgrades-all at all", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + null, + new string[] { "1.0.0", "1.0.0" }), + // Depender blocks latest dependency, but the latest available depender + // doesn't have that limitation, and we upgrade both to latest + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Upgrade-with-identifiers both to bypass current version limitation", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { "Depender", "Dependency" }, + new string[] { "1.1.0", "1.1.0" }), + // Depender blocks latest dependency, but the latest available depender + // doesn't have that limitation, and we upgrade both to latest + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Upgrade-all both to bypass current version limitation", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + null, + new string[] { "1.1.0", "1.1.0" }), + ] + public void RunCommand_VersionDependsUpgrade_UpgradesCorrectly(string description, + string[] instModules, + string[] addlModules, + string[] upgradeIdentifiers, + string[] versionsAfter) + { + // Arrange + var user = new CapturingUser(false, q => true, (msg, objs) => 0); + using (var inst = new DisposableKSP()) + using (var repo = new TemporaryRepository(instModules.Concat(addlModules) + .ToArray())) + using (var repoData = new TemporaryRepositoryData(user, repo.repo)) + using (var config = new FakeConfiguration(inst.KSP, inst.KSP.Name)) + using (var manager = new GameInstanceManager(user, config) + { + CurrentInstance = inst.KSP, + }) + { + var regMgr = RegistryManager.Instance(inst.KSP, repoData.Manager); + regMgr.registry.RepositoriesClear(); + regMgr.registry.RepositoriesAdd(repo.repo); + + // Register installed mods + var instMods = instModules.Select(CkanModule.FromJson) + .ToArray(); + foreach (var fromModule in instMods) + { + regMgr.registry.RegisterModule(fromModule, + Enumerable.Empty(), + inst.KSP, false); + } + // Pre-store mods that might be installed + foreach (var toModule in addlModules.Select(CkanModule.FromJson)) + { + manager.Cache.Store(toModule, TestData.DogeCoinFlagZip(), null); + } + // Simulate passing `--all` + var opts = upgradeIdentifiers != null + ? new UpgradeOptions() + { + modules = upgradeIdentifiers.ToList(), + } + : new UpgradeOptions() + { + modules = new List() {}, + upgrade_all = true, + }; + + // Act + ICommand cmd = new Upgrade(manager, repoData.Manager, user); + cmd.RunCommand(inst.KSP, opts); + + // Assert + CollectionAssert.AreEqual(versionsAfter, + instMods.Select(m => regMgr.registry + .GetInstalledVersion(m.identifier) + .version + .ToString()) + .ToArray(), + description); + + } + } } } diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index 18f236b863..56a68c1170 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -19,7 +19,7 @@ public class RegistryTests private string repoDataDir; private static readonly GameVersionCriteria v0_24_2 = new GameVersionCriteria(GameVersion.Parse("0.24.2")); - private static readonly GameVersionCriteria v0_25_0 = new GameVersionCriteria (GameVersion.Parse("0.25.0")); + private static readonly GameVersionCriteria v0_25_0 = new GameVersionCriteria(GameVersion.Parse("0.25.0")); [SetUp] public void Setup() @@ -268,7 +268,7 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() GameVersionCriteria crit = new GameVersionCriteria(mod.ksp_version); // Act - bool has = registry.HasUpdate(mod.identifier, crit); + bool has = registry.HasUpdate(mod.identifier, crit, out _); // Assert Assert.IsTrue(has, "Can't upgrade manually installed DLL"); @@ -321,7 +321,10 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() GameVersionCriteria crit = new GameVersionCriteria(olderDepMod.ksp_version); // Act - bool has = registry.HasUpdate(olderDepMod.identifier, crit); + bool has = registry.HasUpdate(olderDepMod.identifier, crit, out _, + registry.InstalledModules + .Select(im => im.Module) + .ToList()); // Assert Assert.IsFalse(has, "Upgrade allowed that would break another mod's dependency"); diff --git a/Tests/GUI/Model/GUIMod.cs b/Tests/GUI/Model/GUIMod.cs index 28aae0ef26..09ffa7ffa3 100644 --- a/Tests/GUI/Model/GUIMod.cs +++ b/Tests/GUI/Model/GUIMod.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; #if NET5_0_OR_GREATER using System.Runtime.Versioning; @@ -39,12 +40,12 @@ public void NewGuiModsAreNotSelectedForUpgrade() var mod = new GUIMod(ckan_mod, repoData.Manager, registry, manager.CurrentInstance.VersionCriteria(), null, false, false); - Assert.False(mod.IsUpgradeChecked); + Assert.True(mod.SelectedMod == mod.InstalledMod?.Module); } } [Test] - public void HasUpdateReturnsTrueWhenUpdateAvailable() + public void HasUpdate_UpdateAvailable_ReturnsTrue() { var user = new NullUser(); using (var tidy = new DisposableKSP()) @@ -63,9 +64,14 @@ public void HasUpdateReturnsTrueWhenUpdateAvailable() var registry = new Registry(repoData.Manager, repo.repo); registry.RegisterModule(old_version, Enumerable.Empty(), null, false); + var upgradeableGroups = registry.CheckUpgradeable(tidy.KSP.VersionCriteria(), + new HashSet()); var mod = new GUIMod(old_version, repoData.Manager, registry, tidy.KSP.VersionCriteria(), - null, false, false); + null, false, false) + { + HasUpdate = upgradeableGroups[true].Any(m => m.identifier == old_version.identifier), + }; Assert.True(mod.HasUpdate); } } diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index 800f926bb0..c97e0220e1 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -27,7 +27,7 @@ public class ModListTests public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - Assert.That(item.ComputeUserChangeSet(null, null), Is.Empty); + Assert.That(item.ComputeUserChangeSet(null, null, null), Is.Empty); } [Test] @@ -201,16 +201,16 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() // Mark the mod for install, after completion we will get an exception var otherModule = modules.First(mod => mod.Identifier.Contains("kOS")); - otherModule.IsInstallChecked = true; + otherModule.SelectedMod = otherModule.LatestAvailableMod; - Assert.IsTrue(otherModule.IsInstallChecked); + Assert.IsTrue(otherModule.SelectedMod == otherModule.LatestAvailableMod); Assert.IsFalse(otherModule.IsInstalled); Assert.DoesNotThrow(() => { // Install the "other" module installer.InstallList( - modList.ComputeUserChangeSet(null, null).Select(change => change.Mod).ToList(), + modList.ComputeUserChangeSet(null, null, null).Select(change => change.Mod).ToList(), new RelationshipResolverOptions(), registryManager, ref possibleConfigOnlyDirs, diff --git a/Tests/GUI/ThreadSafetyTests.cs b/Tests/GUI/ThreadSafetyTests.cs index 90528c7a15..b1114c03ce 100644 --- a/Tests/GUI/ThreadSafetyTests.cs +++ b/Tests/GUI/ThreadSafetyTests.cs @@ -7,6 +7,7 @@ using Mono.Cecil; using Mono.Cecil.Cil; using NUnit.Framework; + using CKAN.GUI.Attributes; namespace Tests.GUI