• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 27596707093

16 Jun 2026 05:39AM UTC coverage: 87.892% (+0.2%) from 87.725%
27596707093

push

github

HebaruSan
Merge #4669 Better partial upgrade checking

2019 of 2143 branches covered (94.21%)

Branch coverage included in aggregate %.

74 of 80 new or added lines in 8 files covered. (92.5%)

4 existing lines in 2 files now uncovered.

8637 of 9981 relevant lines covered (86.53%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.64
/GUI/Model/ModList.cs
1
using System;
2
using System.Drawing;
3
using System.Collections.Generic;
4
using System.Linq;
5
using System.Windows.Forms;
6
#if NET5_0_OR_GREATER
7
using System.Runtime.Versioning;
8
#endif
9

10
using Autofac;
11
using log4net;
12

13
using CKAN.Configuration;
14
using CKAN.Versioning;
15
using CKAN.Games;
16
using CKAN.Extensions;
17

18
namespace CKAN.GUI
19
{
20
    /// <summary>
21
    /// The holder of the list of mods to be shown.
22
    /// Should be a pure data model and avoid UI stuff, but it's not there yet.
23
    /// </summary>
24
    #if NET5_0_OR_GREATER
25
    [SupportedOSPlatform("windows")]
26
    #endif
27
    public class ModList
28
    {
29
        /// <summary>
30
        /// Constructs the mod list suitable for display to the user.
31
        /// </summary>
32
        /// <param name="modules">A list of modules that may require updating</param>
33
        /// <param name="instance">Game instance for getting labels</param>
34
        /// <param name="allLabels">All label definitions</param>
35
        /// <param name="allTags">All tag definitions</param>
36
        /// <param name="guiConfig">GUI configuration</param>
37
        /// <param name="graphics">Graphics object for calculating word wrap</param>
38
        /// <param name="mc">Changes the user has made</param>
39
        /// <returns>The mod list</returns>
40
        public ModList(IReadOnlyCollection<GUIMod> modules,
2✔
41
                       GameInstance                instance,
42
                       ModuleLabelList             allLabels,
43
                       ModuleTagList               allTags,
44
                       GUIConfiguration            guiConfig,
45
                       Graphics                    graphics,
46
                       List<ModChange>?            mc = null)
47
        {
48
            this.allLabels        = allLabels;
2✔
49
            this.allTags          = allTags;
2✔
50
            this.guiConfig        = guiConfig;
2✔
51
            this.graphics         = graphics;
2✔
52
            activeSearches        = guiConfig.DefaultSearches
2✔
53
                                             ?.Select(s => ModSearch.Parse(allLabels, instance, s))
×
54
                                              .OfType<ModSearch>()
55
                                              .ToArray()
56
                                             ?? Array.Empty<ModSearch>();
57
            Modules               = modules;
2✔
58
            HasAnyInstalled       = Modules.Any(m => m.IsInstalled);
2✔
59
            full_list_of_mod_rows = Modules.AsParallel()
2✔
60
                                           .ToDictionary(gm => gm.Identifier,
2✔
61
                                                         gm => MakeRow(gm, mc, instance));
2✔
62
        }
2✔
63

64
        // identifier => row
65
        public readonly IReadOnlyCollection<GUIMod>         Modules;
66
        public readonly bool                                HasAnyInstalled;
67
        public readonly Dictionary<string, DataGridViewRow> full_list_of_mod_rows;
68
        public event    Action?                             ModFiltersUpdated;
69

70
        #region Building the mod list
71

72
        /// <summary>
73
        /// Get all the GUI mods for the given instance.
74
        /// </summary>
75
        /// <param name="registry">Registry of the instance</param>
76
        /// <param name="repoData">Repo data of the instance</param>
77
        /// <param name="inst">Game instance</param>
78
        /// <param name="allLabels">All label definitions</param>
79
        /// <param name="cache">Cache for checking if mods are cached</param>
80
        /// <param name="config">GUI config to use</param>
81
        /// <returns>Sequence of GUIMods</returns>
82
        public static IEnumerable<GUIMod> GetGUIMods(IRegistryQuerier      registry,
83
                                                     RepositoryDataManager repoData,
84
                                                     GameInstance          inst,
85
                                                     ModuleLabelList       allLabels,
86
                                                     NetModuleCache        cache,
87
                                                     GUIConfiguration?     config)
88
            => GetGUIMods(registry, repoData, inst,
2✔
89
                          registry.InstalledModules.Select(im => im.identifier)
2✔
90
                                                   .ToHashSet(),
91
                          allLabels,
92
                          cache,
93
                          config?.HideEpochs ?? false, config?.HideV ?? false);
94

95
        private static IEnumerable<GUIMod> GetGUIMods(IRegistryQuerier      registry,
96
                                                      RepositoryDataManager repoData,
97
                                                      GameInstance          inst,
98
                                                      HashSet<string>       installedIdents,
99
                                                      ModuleLabelList       allLabels,
100
                                                      NetModuleCache        cache,
101
                                                      bool                  hideEpochs,
102
                                                      bool                  hideV)
103
            => registry.InstalledModulesByUpgradeability(inst,
2✔
104
                                                         allLabels.HeldIdentifiers(inst)
105
                                                                  .ToHashSet(),
106
                                                         allLabels.IgnoreMissingIdentifiers(inst)
107
                                                                  .ToHashSet())
108
                       .Select(tuple => registry.IsAutodetected(tuple.Item2.identifier)
2✔
109
                                            ? new GUIMod(tuple.Item2, repoData, registry,
110
                                                         inst.StabilityToleranceConfig,
111
                                                         inst, cache, null,
112
                                                         hideEpochs, hideV)
113
                                              {
114
                                                  HasUpdate = tuple.Item1,
115
                                              }
116
                                            : registry.InstalledModule(tuple.Item2.identifier)
117
                                              is InstalledModule found
118
                                                  ? new GUIMod(found, repoData, registry,
119
                                                               inst.StabilityToleranceConfig,
120
                                                               inst, cache, null,
121
                                                               hideEpochs, hideV)
122
                                                    {
123
                                                        HasUpdate = tuple.Item1,
124
                                                    }
125
                                                  : null)
126
                       .OfType<GUIMod>()
127
                       .Concat(registry.CompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
128
                                       .Where(m => !installedIdents.Contains(m.identifier))
2✔
129
                                       .AsParallel()
130
                                       .Where(m => !m.IsDLC)
2✔
131
                                       .Select(m => new GUIMod(m, repoData, registry,
2✔
132
                                                               inst.StabilityToleranceConfig,
133
                                                               inst, cache, null,
134
                                                               hideEpochs, hideV)))
135
                       .Concat(registry.IncompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
136
                                       .Where(m => !installedIdents.Contains(m.identifier))
2✔
137
                                       .AsParallel()
138
                                       .Where(m => !m.IsDLC)
2✔
139
                                       .Select(m => new GUIMod(m, repoData, registry,
2✔
140
                                                               inst.StabilityToleranceConfig,
141
                                                               inst, cache, true,
142
                                                               hideEpochs, hideV)));
143

144
        private DataGridViewRow MakeRow(GUIMod           mod,
145
                                        List<ModChange>? changes,
146
                                        GameInstance     instance)
147
        {
148
            DataGridViewRow item = new DataGridViewRow() { Tag = mod };
2✔
149

150
            item.DefaultCellStyle.BackColor = GetRowBackground(mod, false, instance);
2✔
151
            item.DefaultCellStyle.ForeColor = item.DefaultCellStyle.BackColor.ForeColorForBackColor()
2✔
152
                                              ?? SystemColors.WindowText;
153
            item.DefaultCellStyle.SelectionBackColor = SelectionBlend(item.DefaultCellStyle.BackColor);
2✔
154
            item.DefaultCellStyle.SelectionForeColor = item.DefaultCellStyle.SelectionBackColor.ForeColorForBackColor()
2✔
155
                                                       ?? SystemColors.HighlightText;
156

157
            var myChange = changes?.FindLast(ch => ch.Mod.Equals(mod));
×
158

159
            var selecting = mod.IsAutodetected
2✔
160
                ? new DataGridViewTextBoxCell()
161
                {
162
                    Value = Properties.Resources.MainModListAutoDetected
163
                }
164
                : mod.IsInstallable()
165
                ? (DataGridViewCell) new DataGridViewCheckBoxCell()
166
                {
167
                    Value = myChange == null
168
                        ? mod.IsInstalled
169
                        : myChange.ChangeType == GUIModChangeType.Install
170
                          || (myChange.ChangeType != GUIModChangeType.Remove && mod.IsInstalled)
171
                }
172
                : new DataGridViewTextBoxCell()
173
                {
174
                    Value = "-"
175
                };
176

177
            var autoInstalled = mod.IsInstalled && !mod.IsAutodetected
2✔
178
                ? (DataGridViewCell) new DataGridViewCheckBoxCell()
179
                {
180
                    Value = mod.IsAutoInstalled,
181
                    ToolTipText = Properties.Resources.MainModListAutoInstalledToolTip,
182
                }
183
                : new DataGridViewTextBoxCell()
184
                {
185
                    Value = "-"
186
                };
187

188
            var updating = mod.IsInstallable() && mod.HasUpdate
2✔
189
                ? (DataGridViewCell) new DataGridViewCheckBoxCell()
190
                {
191
                    Value = myChange?.ChangeType == GUIModChangeType.Update
192
                }
193
                : new DataGridViewTextBoxCell()
194
                {
195
                    Value = "-"
196
                };
197

198
            var replacing = (mod.IsInstalled && mod.HasReplacement)
2✔
199
                ? (DataGridViewCell) new DataGridViewCheckBoxCell()
200
                {
201
                    Value = myChange?.ChangeType == GUIModChangeType.Replace
202
                }
203
                : new DataGridViewTextBoxCell()
204
                {
205
                    Value = "-"
206
                };
207

208
            var name           = new DataGridViewTextBoxCell { Value = ToGridText(mod.Name)                                  };
2✔
209
            var author         = new DataGridViewTextBoxCell { Value = ToGridText(string.Join(", ", mod.Authors))            };
2✔
210
            var installVersion = new DataGridViewTextBoxCell { Value = mod.InstalledVersion ?? ""                            };
2✔
211
            var latestVersion  = new DataGridViewTextBoxCell { Value = mod.LatestVersion                                     };
2✔
212
            var compat         = new DataGridViewTextBoxCell { Value = mod.GameCompatibility ?? ""                           };
2✔
213
            var downloadSize   = new DataGridViewTextBoxCell { Value = mod.DownloadSize ?? ""                                };
2✔
214
            var installSize    = new DataGridViewTextBoxCell { Value = mod.InstallSize                                       };
2✔
215
            var releaseDate    = new DataGridViewTextBoxCell { Value = (object?)mod.Module.release_date?.ToLocalTime() ?? "" };
2✔
216
            var installDate    = new DataGridViewTextBoxCell { Value = (object?)mod.InstallDate ?? ""                        };
2✔
217
            var downloadCount  = new DataGridViewTextBoxCell { Value = $"{mod.DownloadCount:N0}"                             };
2✔
218
            var desc           = new DataGridViewTextBoxCell
2✔
219
                                 {
220
                                     Value       = ToGridText(mod.Abstract),
221
                                     ToolTipText = mod.Description is { Length: > 0 }
222
                                                       ? string.Join(Environment.NewLine,
223
                                                                     graphics.WordWrap(mod.Description, 600)
224
                                                                             .Prepend("")
225
                                                                             .Prepend(mod.Abstract))
226
                                                       : mod.Abstract,
227
                                 };
228

229
            item.Cells.AddRange(selecting, autoInstalled, updating, replacing, name, author, installVersion, latestVersion, compat, downloadSize, installSize, releaseDate, installDate, downloadCount, desc);
2✔
230

231
            selecting.ReadOnly     = selecting     is DataGridViewTextBoxCell;
2✔
232
            autoInstalled.ReadOnly = autoInstalled is DataGridViewTextBoxCell;
2✔
233
            updating.ReadOnly      = updating      is DataGridViewTextBoxCell;
2✔
234

235
            return item;
2✔
236
        }
237

238
        private static string ToGridText(string text)
239
            => Platform.IsMono ? text.Replace("&", "&&") : text;
2✔
240

241
        #endregion
242

243
        #region Search & filters
244

245
        public void SetSearches(List<ModSearch> newSearches)
246
        {
247
            if (!SearchesEqual(activeSearches, newSearches))
2✔
248
            {
249
                activeSearches = newSearches;
2✔
250
                guiConfig.DefaultSearches = activeSearches.Select(s => s.Combined ?? "")
2✔
251
                                                          .ToList();
252
                ModFiltersUpdated?.Invoke();
2✔
253
            }
254
        }
2✔
255

256
        public static SavedSearch FilterToSavedSearch(GameInstance    instance,
257
                                                      GUIModFilter    filter,
258
                                                      ModuleLabelList allLabels,
259
                                                      ModuleTag?      tag   = null,
260
                                                      ModuleLabel?    label = null)
261
            => new SavedSearch()
2✔
262
            {
263
                Name   = FilterName(filter, tag, label),
264
                Values = new List<string>()
265
                         {
266
                             new ModSearch(allLabels, instance,
267
                                           filter, tag, label)
268
                                 .Combined
269
                             ?? ""
270
                         },
271
            };
272

273
        private static bool SearchesEqual(IReadOnlyCollection<ModSearch> a,
274
                                          IReadOnlyCollection<ModSearch> b)
275
            => a.SequenceEqual(b);
2✔
276

277
        private static string FilterName(GUIModFilter filter,
278
                                         ModuleTag?   tag   = null,
279
                                         ModuleLabel? label = null)
280
            => filter switch
2✔
281
               {
282
                   GUIModFilter.Compatible               => Properties.Resources.MainFilterCompatible,
2✔
283
                   GUIModFilter.Incompatible             => Properties.Resources.MainFilterIncompatible,
2✔
284
                   GUIModFilter.Installed                => Properties.Resources.MainFilterInstalled,
2✔
285
                   GUIModFilter.NotInstalled             => Properties.Resources.MainFilterNotInstalled,
2✔
286
                   GUIModFilter.InstalledUpdateAvailable => Properties.Resources.MainFilterUpgradeable,
2✔
287
                   GUIModFilter.Replaceable              => Properties.Resources.MainFilterReplaceable,
2✔
288
                   GUIModFilter.Cached                   => Properties.Resources.MainFilterCached,
2✔
289
                   GUIModFilter.Uncached                 => Properties.Resources.MainFilterUncached,
2✔
290
                   GUIModFilter.NewInRepository          => Properties.Resources.MainFilterNew,
2✔
291
                   GUIModFilter.All                      => Properties.Resources.MainFilterAll,
2✔
292
                   GUIModFilter.CustomLabel              => string.Format(Properties.Resources.MainFilterLabel,
2✔
293
                                                                          label?.Name ?? "CUSTOM"),
294
                   GUIModFilter.Tag                      => tag == null
2✔
295
                                                                ? Properties.Resources.MainFilterUntagged
296
                                                                : string.Format(Properties.Resources.MainFilterTag,
297
                                                                                tag.Name),
298
                   _                                     => "",
×
299
               };
300

301
        #endregion
302

303
        #region Visibility
304

305
        // Unlike GUIMod.IsInstalled, DataGridViewRow.Visible can change on the fly without notifying us
306
        public bool HasVisibleInstalled()
307
            => full_list_of_mod_rows.Values.Any(row => ((row.Tag as GUIMod)?.IsInstalled ?? false)
2✔
308
                                                       && row.Visible);
309

310
        public bool IsVisible(GUIMod mod, GameInstance instance, Registry registry)
311
            => activeSearches.IsEmptyOrAny(s => s.Matches(mod))
✔
312
               && !HiddenByTagsOrLabels(mod, instance, registry);
313

314
        private bool HiddenByTagsOrLabels(GUIMod m, GameInstance instance, Registry registry)
315
            // "Hide" labels apply to all non-custom filters
316
            => (allLabels?.LabelsFor(instance.Name)
2✔
317
                                             .Where(l => !LabelInSearches(l) && l.Hide)
2✔
318
                                             .Any(l => l.ContainsModule(instance.Game, m.Identifier))
2✔
319
                                            ?? false)
320
               || (registry.Tags?.Values
321
                                 .Where(t => !TagInSearches(t) && allTags.HiddenTags.Contains(t.Name))
2✔
322
                                 .Any(t => t.ModuleIdentifiers.Contains(m.Identifier))
2✔
323
                                ?? false);
324

325
        private bool TagInSearches(ModuleTag tag)
326
            => activeSearches.Any(s => s.TagNames.Contains(tag.Name));
×
327

328
        private bool LabelInSearches(ModuleLabel label)
329
            => activeSearches.Any(s => s.LabelNames.Contains(label.Name));
×
330

331
        #endregion
332

333
        public int CountModsBySearches(List<ModSearch> searches)
334
            => Modules.Count(mod => searches?.Any(s => s?.Matches(mod) ?? true) ?? true);
×
335

336
        public int CountModsByFilter(GameInstance inst, GUIModFilter filter)
337
            => CountModsBySearches(new List<ModSearch>() { new ModSearch(allLabels, inst, filter, null, null) });
2✔
338

339
        private Color GetRowBackground(GUIMod mod, bool conflicted, GameInstance instance)
340
            => conflicted
2✔
341
                   ? ConflictBackColor
342
                   : Util.BlendColors(allLabels.LabelsFor(instance.Name)
343
                                               .Where(l => l.ContainsModule(instance.Game,
2✔
344
                                                                            mod.Identifier))
345
                                               .Select(l => l.Color)
2✔
346
                                               .OfType<Color>()
347
                                               // No transparent blending
348
                                               .Where(c => c.A == byte.MaxValue)
2✔
349
                                               .ToArray());
350

351
        /// <summary>
352
        /// Update the color and visible state of the given row
353
        /// after it has been added to or removed from a label group
354
        /// </summary>
355
        /// <param name="mod">The mod that needs an update</param>
356
        /// <param name="conflicted">True if mod should have a red background</param>
357
        /// <param name="instance">Game instance for finding labels</param>
358
        /// <param name="registry">Registry for finding mods</param>
359
        public DataGridViewRow? ReapplyLabels(GUIMod mod, bool conflicted,
360
                                              GameInstance instance, Registry registry)
361
        {
362
            if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out DataGridViewRow? row))
2✔
363
            {
364
                row.DefaultCellStyle.BackColor = GetRowBackground(mod, conflicted, instance);
2✔
365
                row.DefaultCellStyle.ForeColor = row.DefaultCellStyle.BackColor.ForeColorForBackColor()
2✔
366
                                                 ?? SystemColors.WindowText;
367
                row.DefaultCellStyle.SelectionBackColor = SelectionBlend(row.DefaultCellStyle.BackColor);
2✔
368
                row.DefaultCellStyle.SelectionForeColor = row.DefaultCellStyle.SelectionBackColor.ForeColorForBackColor()
2✔
369
                                                          ?? SystemColors.HighlightText;
370
                row.Visible = IsVisible(mod, instance, registry);
2✔
371
                return row;
2✔
372
            }
373
            return null;
×
374
        }
375

376
        #region Changesets
377

378
        /// <summary>
379
        /// Get the changes the user has selected in the grid
380
        /// </summary>
381
        /// <param name="registry">Registry containing the installed and available mods</param>
382
        /// <param name="instance">The current game instance</param>
383
        /// <param name="upgradeCol">Column containing the upgrade checkboxes</param>
384
        /// <param name="replaceCol">Column containing the replace checkboxes</param>
385
        /// <returns>Hashset of changes</returns>
386
        public HashSet<ModChange> ComputeUserChangeSet(IRegistryQuerier    registry,
387
                                                       GameInstance        instance,
388
                                                       DataGridViewColumn? upgradeCol,
389
                                                       DataGridViewColumn? replaceCol)
390
        {
391
            log.Debug("Computing user changeset");
2✔
392
            var modChanges = full_list_of_mod_rows.Values
2✔
393
                                                  .SelectMany(row => rowChanges(registry, row, upgradeCol, replaceCol))
2✔
394
                                                  .ToHashSet();
395

396
            // Inter-mod dependencies can block some upgrades, which can sometimes but not always
397
            // be overcome by upgrading both mods. Try to pick the right target versions.
398
            if (modChanges.OfType<ModUpgrade>()
2✔
399
                          // Skip reinstalls
400
                          .Where(upg => upg.Mod != upg.targetMod)
2✔
401
                          .ToArray()
402
                is { Length: > 0 } upgrades)
403
            {
404
                var upgradeable = registry.UpgradeableModules(instance,
2✔
405
                                                              // Hold identifiers not chosen for upgrading
406
                                                              registry.Installed(false)
407
                                                                      .Select(kvp => kvp.Key)
2✔
408
                                                                      .Except(upgrades.Select(ch => ch.Mod.identifier))
2✔
409
                                                                      .ToHashSet(),
410
                                                              allLabels.IgnoreMissingIdentifiers(instance)
411
                                                                       .ToHashSet())
412
                                          .ToDictionary(m => m.identifier,
2✔
413
                                                        m => m);
2✔
414
                foreach (var change in upgrades)
6✔
415
                {
416
                    change.targetMod = upgradeable.TryGetValue(change.Mod.identifier,
2✔
417
                                                               out CkanModule? allowedMod)
418
                                           // Upgrade to the version the registry says we should
419
                                           ? allowedMod
420
                                           // Not upgradeable!
421
                                           : change.Mod;
422
                    if (change.Mod == change.targetMod)
2✔
423
                    {
424
                        // This upgrade was voided by dependencies or conflicts
425
                        modChanges.Remove(change);
×
426
                    }
427
                }
428
            }
429

430
            return modChanges;
2✔
431
        }
432

433
        private static IEnumerable<ModChange> rowChanges(IRegistryQuerier    registry,
434
                                                         DataGridViewRow     row,
435
                                                         DataGridViewColumn? upgradeCol,
436
                                                         DataGridViewColumn? replaceCol)
437
            => row.Tag is GUIMod gmod
2✔
438
                   ? gmod.GetModChanges(upgradeCol?.Visible == true
439
                                        && row.Cells[upgradeCol.Index] is DataGridViewCheckBoxCell upgradeCell
440
                                        && upgradeCell.Value is true,
441
                                        replaceCol?.Visible == true
442
                                        && row.Cells[replaceCol.Index] is DataGridViewCheckBoxCell replaceCell
443
                                        && replaceCell.Value is true,
444
                                        registry.MetadataChanged(gmod.Identifier, out bool installedFilesChanged),
445
                                        installedFilesChanged)
446
                   : Enumerable.Empty<ModChange>();
447

448
        public static Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>> ComputeFullChangeSetFromUserChangeSet(
449
            IRegistryQuerier         registry,
450
            HashSet<ModChange>       changeSet,
451
            IConfiguration           coreConfig,
452
            GameInstance             instance)
453
            => ComputeFullChangeSetFromUserChangeSet(registry, changeSet, coreConfig,
2✔
454
                                                     instance.Game, instance.StabilityToleranceConfig, instance.VersionCriteria());
455

456
        /// <summary>
457
        /// Returns a changeset and conflicts based on the selections of the user.
458
        /// </summary>
459
        /// <param name="registry">The registry for getting available mods</param>
460
        /// <param name="changeSet">User's choices of installation and removal</param>
461
        /// <param name="coreConfig">Core configuration</param>
462
        /// <param name="game">Game of the game instance</param>
463
        /// <param name="stabilityTolerance">Prerelease configuration</param>
464
        /// <param name="version">The version of the current game instance</param>
465
        /// <returns>
466
        /// Tuple of:
467
        /// 1. Full changeset, including auto-installed dependencies
468
        /// 2. Mapping from conflicting mods to description of the conflict
469
        /// 3. Descriptions of all conflicts
470
        /// </returns>
471
        public static Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>> ComputeFullChangeSetFromUserChangeSet(
472
            IRegistryQuerier         registry,
473
            HashSet<ModChange>       changeSet,
474
            IConfiguration           coreConfig,
475
            IGame                    game,
476
            StabilityToleranceConfig stabilityTolerance,
477
            GameVersionCriteria      version)
478
        {
479
            changeSet.UnionWith(changeSet.Where(ch => ch.ChangeType == GUIModChangeType.Replace)
2✔
480
                                         .Select(ch => registry.GetReplacement(ch.Mod, stabilityTolerance, version))
×
481
                                         .OfType<ModuleReplacement>()
482
                                         .GroupBy(repl => repl.ReplaceWith)
×
483
                                         .Select(grp => new ModChange(grp.Key, GUIModChangeType.Install,
×
484
                                                                      grp.Select(repl => new SelectionReason.Replacement(repl.ToReplace)),
×
485
                                                                      coreConfig)));
486

487
            var toInstall     = new List<CkanModule>();
2✔
488
            var toRemove      = new HashSet<CkanModule>();
2✔
489
            var extraInstalls = new HashSet<CkanModule>();
2✔
490
            foreach (var change in changeSet)
6✔
491
            {
492
                switch (change.ChangeType)
2✔
493
                {
494
                    case GUIModChangeType.None:
495
                        break;
496
                    case GUIModChangeType.Update:
497
                        var mod = (change as ModUpgrade)?.targetMod ?? change.Mod;
×
498
                        toInstall.Add(mod);
×
499
                        extraInstalls.Add(mod);
×
500
                        break;
×
501
                    case GUIModChangeType.Install:
502
                        toInstall.Add(change.Mod);
2✔
503
                        break;
2✔
504
                    case GUIModChangeType.Remove:
505
                        toRemove.Add(change.Mod);
×
506
                        break;
×
507
                    case GUIModChangeType.Replace:
508
                        if (registry.GetReplacement(change.Mod, stabilityTolerance, version) is ModuleReplacement repl)
×
509
                        {
510
                            toRemove.Add(repl.ToReplace);
×
511
                            extraInstalls.Add(repl.ReplaceWith);
×
512
                        }
513
                        break;
514
                }
515
            }
516

517
            // Check for depending mods if any are still left
518
            if (!registry.InstalledModules.Select(im => im.Module)
2✔
519
                                          .All(toRemove.Contains))
520
            {
521
                var installedModules = registry.InstalledModules.ToDictionary(
2✔
522
                                           imod => imod.Module.identifier,
2✔
523
                                           imod => imod.Module);
2✔
524
                foreach (var dependent in registry.FindReverseDependencies(
4✔
NEW
525
                                              toRemove.Select(mod => mod.identifier)
×
NEW
526
                                                      .Except(toInstall.Select(m => m.identifier))
×
527
                                                      .ToList(),
528
                                              toInstall))
529
                {
NEW
530
                    if (!changeSet.Any(ch => ch.ChangeType == GUIModChangeType.Replace
×
531
                                             && ch.Mod.identifier == dependent)
532
                        && installedModules.TryGetValue(dependent, out CkanModule? depMod)
533
                        && (registry.GetModuleByVersion(depMod.identifier, depMod.version)
534
                            ?? registry.InstalledModule(dependent)?.Module)
535
                            is CkanModule modByVer)
536
                    {
NEW
537
                        changeSet.Add(new ModChange(modByVer, GUIModChangeType.Remove,
×
538
                                                    new SelectionReason.DependencyRemoved(),
539
                                                    coreConfig));
NEW
540
                        toRemove.Add(modByVer);
×
541
                    }
542
                }
543
                // Check for auto-installed dependencies if any mods are still left
544
                if (!registry.InstalledModules.Select(im => im.Module)
2✔
545
                                              .All(toRemove.Contains))
546
                {
547
                    foreach (var im in registry.FindRemovableAutoInstalled(
6✔
548
                        InstalledAfterChanges(registry, changeSet).ToArray(),
549
                        Array.Empty<CkanModule>(), game, stabilityTolerance, version))
550
                    {
551
                        changeSet.Add(new ModChange(im.Module, GUIModChangeType.Remove, new SelectionReason.NoLongerUsed(),
2✔
552
                                                    coreConfig));
553
                        toRemove.Add(im.Module);
2✔
554
                    }
555
                }
556
            }
557

558
            // Get as many dependencies as we can, but leave decisions and prompts for installation time
559
            var resolver = new RelationshipResolver(toInstall, toRemove,
2✔
560
                                                    conflictOptions(stabilityTolerance),
561
                                                    registry, game, version);
562

563
            // Replace Install entries in changeset with the ones from resolver to get all the reasons
564
            return new Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>>(
2✔
565
                changeSet.Where(ch => !(ch.ChangeType is GUIModChangeType.Install
2✔
566
                                        // Leave in replacements
567
                                        && !ch.Reasons.Any(r => r is SelectionReason.Replacement)))
2✔
568
                         .OrderBy(ch => ch.Mod.identifier)
2✔
569
                         .Union(resolver.ModList()
570
                                        // Changeset already contains changes for these
571
                                        .Except(extraInstalls)
572
                                        .Select(m => new ModChange(m, GUIModChangeType.Install, resolver.ReasonsFor(m),
2✔
573
                                                                   coreConfig)))
574
                         .ToArray(),
575
                resolver.ConflictList,
576
                resolver.ConflictDescriptions.ToList());
577
        }
578

579
        /// <summary>
580
        /// Get the InstalledModules that we'll have after the changeset,
581
        /// not including dependencies
582
        /// </summary>
583
        /// <param name="registry">Registry with currently installed modules</param>
584
        /// <param name="changeSet">Changes to be made to the installed modules</param>
585
        private static IEnumerable<InstalledModule> InstalledAfterChanges(
586
            IRegistryQuerier               registry,
587
            IReadOnlyCollection<ModChange> changeSet)
588
        {
589
            var removingIdents = changeSet
2✔
UNCOV
590
                .Where(ch => ch.ChangeType != GUIModChangeType.Install)
×
591
                .Select(ch => ch.Mod.identifier)
×
592
                .ToHashSet();
593
            return registry.InstalledModules
2✔
594
                .Where(im => !removingIdents.Contains(im.identifier))
2✔
595
                .Concat(changeSet
UNCOV
596
                    .Where(ch => ch.ChangeType is not GUIModChangeType.Remove
×
597
                                              and not GUIModChangeType.Replace)
UNCOV
598
                    .Select(ch => new InstalledModule(
×
599
                        null,
600
                        (ch as ModUpgrade)?.targetMod ?? ch.Mod,
601
                        Enumerable.Empty<string>(),
602
                        false)));
603
        }
604

605
        private static RelationshipResolverOptions conflictOptions(StabilityToleranceConfig stabilityTolerance)
606
            => new RelationshipResolverOptions(stabilityTolerance)
2✔
607
            {
608
                without_toomanyprovides_kraken = true,
609
                proceed_with_inconsistencies   = true,
610
                without_enforce_consistency    = true,
611
                with_recommends                = false
612
            };
613

614
        #endregion
615

616
        #region Upgradeability
617

618
        /// <summary>
619
        /// Check upgradeability of all rows and set GUIMod.HasUpdate appropriately
620
        /// </summary>
621
        /// <param name="inst">Current game instance</param>
622
        /// <param name="registry">Current instance's registry</param>
623
        /// <param name="ChangeSet">Currently pending changeset</param>
624
        /// <param name="rows">The grid rows in case we need to replace some</param>
625
        /// <returns>true if any mod can be updated, false otherwise</returns>
626
        public bool ResetHasUpdate(GameInstance              inst,
627
                                   IRegistryQuerier          registry,
628
                                   List<ModChange>?          ChangeSet,
629
                                   DataGridViewRowCollection rows)
630
        {
631
            var dlls = registry.InstalledDlls.ToList();
2✔
632
            bool hasUpgradeable = false;
2✔
633
            foreach (var (upgradeable, module) in registry.InstalledModulesByUpgradeability(
6✔
634
                                                      inst,
635
                                                      allLabels.HeldIdentifiers(inst)
636
                                                               .ToHashSet(),
637
                                                      allLabels.IgnoreMissingIdentifiers(inst)
638
                                                               .ToHashSet()))
639
            {
640
                hasUpgradeable = hasUpgradeable || upgradeable;
2✔
641
                dlls.Remove(module.identifier);
2✔
642
                CheckRowUpgradeable(inst, ChangeSet, rows, module.identifier, upgradeable);
2✔
643
            }
644
            // AD mods don't have CkanModules in the return value of CheckUpgradeable
645
            foreach (var ident in dlls)
4✔
646
            {
647
                CheckRowUpgradeable(inst, ChangeSet, rows, ident, false);
×
648
            }
649
            return hasUpgradeable;
2✔
650
        }
651

652
        private void CheckRowUpgradeable(GameInstance              inst,
653
                                         List<ModChange>?          ChangeSet,
654
                                         DataGridViewRowCollection rows,
655
                                         string                    ident,
656
                                         bool                      upgradeable)
657
        {
658
            if (full_list_of_mod_rows.TryGetValue(ident, out DataGridViewRow? row)
2✔
659
                && row.Tag is GUIMod gmod
660
                && gmod.HasUpdate != upgradeable)
661
            {
662
                gmod.HasUpdate = upgradeable;
×
663
                if (row.Visible)
×
664
                {
665
                    // Swap whether the row has an upgrade checkbox
666
                    var newRow = full_list_of_mod_rows[ident] = MakeRow(gmod, ChangeSet, inst);
×
667
                    var rowIndex = row.Index;
×
668
                    var selected = row.Selected;
×
669
                    rows.Remove(row);
×
670
                    rows.Insert(rowIndex, newRow);
×
671
                    if (selected)
×
672
                    {
673
                        rows[rowIndex].Selected = true;
×
674
                    }
675
                }
676
            }
677
        }
2✔
678

679
        #endregion
680

681
        private static Color SelectionBlend(Color c)
682
            => c == Color.Empty
2✔
683
                ? SystemColors.Highlight
684
                : SystemColors.Highlight.AlphaBlendWith(selectionAlpha, c);
685

686
        private const float selectionAlpha = 0.4f;
687

688
        private readonly ModuleLabelList                allLabels;
689
        private readonly ModuleTagList                  allTags;
690
        private readonly GUIConfiguration               guiConfig;
691
        private readonly Graphics                       graphics;
692
        private          IReadOnlyCollection<ModSearch> activeSearches;
693

694
        public  static readonly Color ConflictBackColor = Color.FromArgb(255, 64, 64);
2✔
695
        public  static readonly Color ConflictForeColor = ConflictBackColor.ForeColorForBackColor() ?? SystemColors.WindowText;
2✔
696

697
        private static readonly ILog log = LogManager.GetLogger(typeof(ModList));
2✔
698
    }
699
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc