diff --git a/CHANGELOG.md b/CHANGELOG.md index f2433a90f..30ead131b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to this project will be documented in this file. - [Multiple] New Crowdin updates (#4019 by: Olympic1, vinix38; reviewed: HebaruSan) - [Core] Support Windows KSP1 instances on Linux (#4044 by: HebaruSan) - [GUI] I18n updates from Crowdin (#4050 by: HebaruSan) -- [Multiple] Better version specific relationships at install and upgrade (#4023 by: HebaruSan) +- [Multiple] Better version specific relationships at install and upgrade (#4023, #4152 by: HebaruSan) - [GUI] Proportional, granular progress updates for installing (#4055 by: HebaruSan) - [GUI] Modpack compatibility prompt, GameComparator clean-up (#4056 by: HebaruSan) - [ConsoleUI] Add downloads column for ConsoleUI (#4063 by: HebaruSan) diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index ed86ba4a8..62d783512 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1514,20 +1514,20 @@ public void ImportFiles(HashSet files, IUser user, Action HashSet installable = new HashSet(); List deletable = new List(); // Get the mapping of known hashes to modules - Dictionary> index = registry.GetSha1Index(); + var index = registry.GetDownloadHashesIndex(); int i = 0; foreach (FileInfo f in files) { int percent = i * 100 / files.Count; user.RaiseProgress(string.Format(Properties.Resources.ModuleInstallerImporting, f.Name, percent), percent); - // Calc SHA-1 sum - string sha1 = Cache.GetFileHashSha1(f.FullName, new Progress(bytes => {})); - // Find SHA-1 sum in registry (potentially multiple) - if (index.ContainsKey(sha1)) + // Find SHA-256 or SHA-1 sum in registry (potentially multiple) + if (index.TryGetValue(Cache.GetFileHashSha256(f.FullName, new Progress(bytes => {})), + out List matches) + || index.TryGetValue(Cache.GetFileHashSha1(f.FullName, new Progress(bytes => {})), + out matches)) { deletable.Add(f); - List matches = index[sha1]; - foreach (CkanModule mod in matches) + foreach (var mod in matches) { if (mod.IsCompatible(instance.VersionCriteria())) { diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 98097b979..df152c895 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -302,7 +302,7 @@ public void EnforceSizeLimit(long bytes, Registry registry) ); // This object lets us find the modules associated with a cached file - Dictionary> hashMap = registry.GetDownloadHashIndex(); + var hashMap = registry.GetDownloadUrlHashIndex(); // Prune the module lists to only those that are compatible foreach (var kvp in hashMap) diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index fdf9759d9..86d8a5b48 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -323,4 +323,6 @@ Free up space on that device or change your settings to use another location. {0} {1} ({2}, {3} remaining) * Upgrade: {0} {1} to {2} ({3}, {4} remaining) installed-{0} + {0} (installed {1}) + {0} (installed {1}, auto-installed) diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index 0d1434ebc..264d65dc8 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -45,10 +45,10 @@ public class InstalledModule [JsonProperty] private Dictionary installed_files; - public IEnumerable Files => installed_files.Keys; - public string identifier => source_module.identifier; - public CkanModule Module => source_module; - public DateTime InstallTime => install_time; + public IEnumerable Files => installed_files.Keys; + public string identifier => source_module.identifier; + public CkanModule Module => source_module; + public DateTime InstallTime => install_time; public bool AutoInstalled { @@ -125,7 +125,7 @@ public void Renormalise(GameInstance ksp) // We need case insensitive path matching on Windows var normalised_installed_files = new Dictionary(Platform.PathComparer); - foreach (KeyValuePair tuple in installed_files) + foreach (var tuple in installed_files) { string path = CKANPathUtils.NormalizePath(tuple.Key); @@ -141,5 +141,11 @@ public void Renormalise(GameInstance ksp) } #endregion + + public override string ToString() + => string.Format(AutoInstalled ? Properties.Resources.InstalledModuleToStringAutoInstalled + : Properties.Resources.InstalledModuleToString, + Module, + InstallTime); } } diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 4ceea0b84..7362c8c90 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -496,6 +496,12 @@ private void EnlistWithTransaction() [JsonIgnore] private HashSet untagged; + [JsonIgnore] + private Dictionary> downloadHashesIndex; + + [JsonIgnore] + private Dictionary> downloadUrlHashIndex; + // Index of which mods provide what, format: // providers[provided] = { provider1, provider2, ... } // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. @@ -508,10 +514,12 @@ private void InvalidateAvailableModCaches() // These member variables hold references to data from our repo data manager // that reflects how the available modules look to this instance. // Clear them when we have reason to believe the upstream available modules have changed. - providers = null; - sorter = null; - tags = null; - untagged = null; + providers = null; + sorter = null; + tags = null; + untagged = null; + downloadHashesIndex = null; + downloadUrlHashIndex = null; } private void InvalidateInstalledCaches() @@ -1069,7 +1077,7 @@ internal Dictionary ProvidedByInstalled() /// /// /// - public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides=true) + public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides = true) { // If it's genuinely installed, return the details we have. // (Includes DLCs) @@ -1100,27 +1108,17 @@ public ModuleVersion InstalledVersion(string modIdentifier, bool with_provides=t /// /// public CkanModule GetInstalledVersion(string mod_identifier) - => installed_modules.TryGetValue(mod_identifier, out InstalledModule installedModule) - ? installedModule.Module - : null; + => InstalledModule(mod_identifier)?.Module; /// /// Returns the module which owns this file, or null if not known. /// Throws a PathErrorKraken if an absolute path is provided. /// - public string FileOwner(string file) - { - file = CKANPathUtils.NormalizePath(file); - - if (Path.IsPathRooted(file)) - { - throw new PathErrorKraken( - file, - "KSPUtils.FileOwner can only work with relative paths."); - } - - return installed_files.TryGetValue(file, out string fileOwner) ? fileOwner : null; - } + public InstalledModule FileOwner(string file) + => installed_files.TryGetValue(CKANPathUtils.NormalizePath(file), + out string fileOwner) + ? InstalledModule(fileOwner) + : null; /// /// @@ -1232,71 +1230,52 @@ public IEnumerable FindReverseDependencies( satisfiedFilter); /// - /// Get a dictionary of all mod versions indexed by their downloads' SHA-1 hash. + /// Get a dictionary of all mod versions indexed by their downloads' SHA-256 and SHA-1 hashes. /// Useful for finding the mods for a group of files without repeatedly searching the entire registry. /// /// - /// dictionary[sha1] = {mod1, mod2, mod3}; + /// dictionary[sha256 or sha1] = {mod1, mod2, mod3}; /// - public Dictionary> GetSha1Index() + public Dictionary> GetDownloadHashesIndex() + => downloadHashesIndex = downloadHashesIndex + ?? repoDataMgr.GetAllAvailableModules(repositories.Values) + .SelectMany(availMod => availMod.module_version.Values) + .SelectMany(ModWithDownloadHashes) + .GroupBy(tuple => tuple.Item1, + tuple => tuple.Item2) + .ToDictionary(grp => grp.Key, + grp => grp.ToList()); + + private IEnumerable> ModWithDownloadHashes(CkanModule m) { - var index = new Dictionary>(); - foreach (var am in repoDataMgr.GetAllAvailableModules(repositories.Values)) + if (!string.IsNullOrEmpty(m.download_hash?.sha256)) { - foreach (var kvp2 in am.module_version) - { - CkanModule mod = kvp2.Value; - if (mod.download_hash != null) - { - if (index.ContainsKey(mod.download_hash.sha1)) - { - index[mod.download_hash.sha1].Add(mod); - } - else - { - index.Add(mod.download_hash.sha1, new List() {mod}); - } - } - } + yield return new Tuple(m.download_hash.sha256, m); + } + if (!string.IsNullOrEmpty(m.download_hash?.sha1)) + { + yield return new Tuple(m.download_hash.sha1, m); } - return index; } /// - /// Get a dictionary of all mod versions indexed by their download URLs' hash. + /// Get a dictionary of all mod versions indexed by their download URLs' hashes. /// Useful for finding the mods for a group of URLs without repeatedly searching the entire registry. /// /// /// dictionary[urlHash] = {mod1, mod2, mod3}; /// - public Dictionary> GetDownloadHashIndex() - { - var index = new Dictionary>(); - foreach (var am in repoDataMgr?.GetAllAvailableModules(repositories.Values) + public Dictionary> GetDownloadUrlHashIndex() + => downloadUrlHashIndex = downloadUrlHashIndex + ?? (repoDataMgr?.GetAllAvailableModules(repositories.Values) ?? Enumerable.Empty()) - { - foreach (var kvp2 in am.module_version) - { - CkanModule mod = kvp2.Value; - if (mod.download != null) - { - foreach (var dlUri in mod.download) - { - string hash = NetFileCache.CreateURLHash(dlUri); - if (index.ContainsKey(hash)) - { - index[hash].Add(mod); - } - else - { - index.Add(hash, new List() {mod}); - } - } - } - } - } - return index; - } + .SelectMany(am => am.module_version.Values) + .Where(m => m.download != null && m.download.Count > 0) + .SelectMany(m => m.download.Select(url => new Tuple(url, m))) + .GroupBy(tuple => tuple.Item1, + tuple => tuple.Item2) + .ToDictionary(grp => NetFileCache.CreateURLHash(grp.Key), + grp => grp.ToList()); /// /// Return all hosts from latest versions of all available modules, diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index 58d6e1bfe..e5bdddf0c 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -276,8 +276,8 @@ public class FileExistsKraken : Kraken // These aren't set at construction time, but exist so that we can decorate the // kraken as appropriate. - public CkanModule installingModule; - public string owningModule; + public CkanModule installingModule; + public InstalledModule owningModule; public FileExistsKraken(string filename, string reason = null, Exception innerException = null) : base(reason, innerException) diff --git a/Dockerfile.metadata b/Dockerfile.metadata index d51c1dee0..02292063d 100644 --- a/Dockerfile.metadata +++ b/Dockerfile.metadata @@ -2,7 +2,7 @@ FROM ubuntu:22.04 as base # Don't prompt for time zone -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND noninteractive # Put user-installed Python code in path ENV PATH "$PATH:/root/.local/bin" @@ -33,7 +33,8 @@ RUN apt-get install -y --no-install-recommends \ python3-pip python3-setuptools python3-dev # Install the meta tester's Python code and its Infra dep -ENV PIP_ROOT_USER_ACTION=ignore +ENV PIP_ROOT_USER_ACTION ignore +ENV PIP_BREAK_SYSTEM_PACKAGES 1 RUN pip3 install --upgrade pip RUN pip3 install 'git+https://github.com/KSP-CKAN/NetKAN-Infra#subdirectory=netkan' RUN pip3 install 'git+https://github.com/KSP-CKAN/xKAN-meta_testing' diff --git a/Dockerfile.netkan b/Dockerfile.netkan index eabe7ef66..b874cffc7 100644 --- a/Dockerfile.netkan +++ b/Dockerfile.netkan @@ -1,7 +1,7 @@ FROM ubuntu:22.04 # Don't prompt for time zone -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND noninteractive # Set up Mono's APT repo RUN apt-get update \ diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 586715084..38e6b3b4c 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -523,7 +523,7 @@ public void MarkAllUpdates() { if (!Main.Instance.LabelsHeld(gmod.Identifier)) { - gmod.SelectedMod = gmod.LatestAvailableMod; + gmod.SelectedMod = gmod.LatestCompatibleMod; } } } @@ -881,7 +881,7 @@ private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e case "Installed": gmod.SelectedMod = nowChecked ? gmod.SelectedMod ?? gmod.InstalledMod?.Module - ?? gmod.LatestAvailableMod + ?? gmod.LatestCompatibleMod : null; break; case "UpdateCol": @@ -890,10 +890,10 @@ private void ModGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e && (gmod.InstalledMod == null || gmod.InstalledMod.Module.version < gmod.SelectedMod.version) ? gmod.SelectedMod - : gmod.LatestAvailableMod + : gmod.LatestCompatibleMod : gmod.InstalledMod?.Module; - if (nowChecked && gmod.SelectedMod == gmod.LatestAvailableMod) + if (nowChecked && gmod.SelectedMod == gmod.LatestCompatibleMod) { // Reinstall, force update without change UpdateChangeSetAndConflicts(currentInstance, diff --git a/GUI/Controls/ModInfoTabs/Metadata.cs b/GUI/Controls/ModInfoTabs/Metadata.cs index d12866c33..3d18ef49e 100644 --- a/GUI/Controls/ModInfoTabs/Metadata.cs +++ b/GUI/Controls/ModInfoTabs/Metadata.cs @@ -49,7 +49,9 @@ public void UpdateModInfo(GUIMod gui_module) MetadataModuleReleaseStatusTextBox.Text = module.release_status.ToString(); } - var compatMod = gui_module.LatestCompatibleMod ?? gui_module.LatestAvailableMod ?? gui_module.ToModule(); + var compatMod = gui_module.LatestCompatibleMod + ?? gui_module.LatestAvailableMod + ?? gui_module.ToModule(); MetadataModuleGameCompatibilityTextBox.Text = string.Format( Properties.Resources.GUIModGameCompatibilityLong, gui_module.GameCompatibility, diff --git a/GUI/Dialogs/AskUserForAutoUpdatesDialog.resx b/GUI/Dialogs/AskUserForAutoUpdatesDialog.resx index 973c0ae44..e7c3d1ba4 100644 --- a/GUI/Dialogs/AskUserForAutoUpdatesDialog.resx +++ b/GUI/Dialogs/AskUserForAutoUpdatesDialog.resx @@ -117,7 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Do you wish for CKAN to automatically check for updates on start-up? (Can be changed later from Settings) + Would you like CKAN to check for new versions of CKAN automatically at start-up? + +You can check for new versions of CKAN manually or change this setting later in the Settings window. Yes, check for updates No Check for updates diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 8806ed22a..2b93f1143 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -717,7 +717,7 @@ private void InstallFromCkanFiles(string[] files) rel.ExactMatch(registry_manager.registry, crit, installed, toInstall) // Otherwise look for incompatible ?? rel.ExactMatch(registry_manager.registry, null, installed, toInstall)) - .Where(mod => mod != null)); + .OfType()); } toInstall.Add(module); } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index eef494251..b0a6f2c36 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -231,10 +231,10 @@ https://github.com/KSP-CKAN/NetKAN/issues/new/choose Please include the following information in your report: -File : {0} -Installing Mod : {1} +File: {0} +Installing Mod: {1} Owning Mod: {2} -CKAN Version : {3} +CKAN Version: {3} Oh no! It looks like you're trying to install a mod which is already installed, @@ -329,7 +329,9 @@ If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/ Repository update failed! Repositories successfully updated. Repositories updated, but found modules that require a new version of CKAN. Checking for updates... - Would you like CKAN to refresh the modlist every time it is loaded? (You can always manually refresh using the button up top.) + Would you like CKAN to refresh the mod list automatically every time it is loaded? + +You can refresh the mod list manually with the Refresh button at the top, and you can change this setting later in the Settings. If you disable automatic refreshes, it's a good idea to refresh every few days. {0} update(s) available Click to upgrade Resume