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

KSP-CKAN / CKAN / 20979898649

14 Jan 2026 02:16AM UTC coverage: 85.236% (-0.09%) from 85.324%
20979898649

push

github

HebaruSan
Merge #4496 Allow newly installed versions to satisfy deps

1994 of 2159 branches covered (92.36%)

Branch coverage included in aggregate %.

87 of 101 new or added lines in 2 files covered. (86.14%)

19 existing lines in 3 files now uncovered.

11931 of 14178 relevant lines covered (84.15%)

1.76 hits per line

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

74.16
/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="coreConfig">Core configuration</param>
37
        /// <param name="guiConfig">GUI configuration</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
                       IConfiguration              coreConfig,
45
                       GUIConfiguration            guiConfig,
46
                       List<ModChange>?            mc = null)
47
        {
2✔
48
            this.allLabels        = allLabels;
2✔
49
            this.allTags          = allTags;
2✔
50
            this.coreConfig       = coreConfig;
2✔
51
            this.guiConfig        = guiConfig;
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.CheckUpgradeable(inst,
2✔
104
                                         allLabels.HeldIdentifiers(inst)
105
                                                  .ToHashSet(),
106
                                         allLabels.IgnoreMissingIdentifiers(inst)
107
                                                  .ToHashSet())
108
                       .SelectMany(kvp => kvp.Value
2✔
109
                                             .Select(mod => registry.IsAutodetected(mod.identifier)
2✔
110
                                                            ? new GUIMod(mod, repoData, registry,
111
                                                                         inst.StabilityToleranceConfig,
112
                                                                         inst, cache, null,
113
                                                                         hideEpochs, hideV)
114
                                                              {
115
                                                                  HasUpdate = kvp.Key,
116
                                                              }
117
                                                            : registry.InstalledModule(mod.identifier)
118
                                                              is InstalledModule found
119
                                                              ? new GUIMod(found,
120
                                                                           repoData, registry,
121
                                                                           inst.StabilityToleranceConfig,
122
                                                                           inst, cache, null,
123
                                                                           hideEpochs, hideV)
124
                                                              {
125
                                                                  HasUpdate = kvp.Key,
126
                                                              }
127
                                                              : null))
128
                       .OfType<GUIMod>()
129
                       .Concat(registry.CompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
130
                                       .Where(m => !installedIdents.Contains(m.identifier))
2✔
131
                                       .AsParallel()
132
                                       .Where(m => !m.IsDLC)
2✔
133
                                       .Select(m => new GUIMod(m, repoData, registry,
2✔
134
                                                               inst.StabilityToleranceConfig,
135
                                                               inst, cache, null,
136
                                                               hideEpochs, hideV)))
137
                       .Concat(registry.IncompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
138
                                       .Where(m => !installedIdents.Contains(m.identifier))
2✔
139
                                       .AsParallel()
140
                                       .Where(m => !m.IsDLC)
2✔
141
                                       .Select(m => new GUIMod(m, repoData, registry,
2✔
142
                                                               inst.StabilityToleranceConfig,
143
                                                               inst, cache, true,
144
                                                               hideEpochs, hideV)));
145

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

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

NEW
159
            var myChange = changes?.FindLast(ch => ch.Mod.Equals(mod));
×
160

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

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

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

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

210
            var name   = new DataGridViewTextBoxCell { Value = ToGridText(mod.Name)                       };
2✔
211
            var author = new DataGridViewTextBoxCell { Value = ToGridText(string.Join(", ", mod.Authors)) };
2✔
212

213
            var installVersion = new DataGridViewTextBoxCell()
2✔
214
            {
215
                Value = mod.InstalledVersion
216
            };
217

218
            var latestVersion = new DataGridViewTextBoxCell()
2✔
219
            {
220
                Value = mod.LatestVersion
221
            };
222

223
            var downloadCount = new DataGridViewTextBoxCell { Value = $"{mod.DownloadCount:N0}"   };
2✔
224
            var compat        = new DataGridViewTextBoxCell { Value = mod.GameCompatibility       };
2✔
225
            var downloadSize  = new DataGridViewTextBoxCell { Value = mod.DownloadSize            };
2✔
226
            var installSize   = new DataGridViewTextBoxCell { Value = mod.InstallSize             };
2✔
227
            var releaseDate   = new DataGridViewTextBoxCell { Value = mod.Module.release_date?.ToLocalTime() };
2✔
228
            var installDate   = new DataGridViewTextBoxCell { Value = mod.InstallDate             };
2✔
229
            var desc          = new DataGridViewTextBoxCell { Value = ToGridText(mod.Abstract)    };
2✔
230

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

233
            selecting.ReadOnly     = selecting     is DataGridViewTextBoxCell;
2✔
234
            autoInstalled.ReadOnly = autoInstalled is DataGridViewTextBoxCell;
2✔
235
            updating.ReadOnly      = updating      is DataGridViewTextBoxCell;
2✔
236

237
            return item;
2✔
238
        }
2✔
239

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

243
        #endregion
244

245
        #region Search & filters
246

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

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

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

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

303
        #endregion
304

305
        #region Visibility
306

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

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

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

327
        private bool TagInSearches(ModuleTag tag)
NEW
328
            => activeSearches.Any(s => s.TagNames.Contains(tag.Name));
×
329

330
        private bool LabelInSearches(ModuleLabel label)
NEW
331
            => activeSearches.Any(s => s.LabelNames.Contains(label.Name));
×
332

333
        #endregion
334

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

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

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

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

378
        #region Changesets
379

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

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

433
            return modChanges;
2✔
434
        }
2✔
435

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

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

479
            var toInstall     = new List<CkanModule>();
2✔
480
            var toRemove      = new HashSet<CkanModule>();
2✔
481
            var extraInstalls = new HashSet<CkanModule>();
2✔
482
            foreach (var change in changeSet)
8✔
483
            {
2✔
484
                switch (change.ChangeType)
2✔
485
                {
486
                    case GUIModChangeType.None:
487
                        break;
×
488
                    case GUIModChangeType.Update:
489
                        var mod = (change as ModUpgrade)?.targetMod ?? change.Mod;
×
NEW
490
                        toInstall.Add(mod);
×
491
                        extraInstalls.Add(mod);
×
492
                        break;
×
493
                    case GUIModChangeType.Install:
494
                        toInstall.Add(change.Mod);
2✔
495
                        break;
2✔
496
                    case GUIModChangeType.Remove:
NEW
497
                        toRemove.Add(change.Mod);
×
498
                        break;
×
499
                    case GUIModChangeType.Replace:
500
                        if (registry.GetReplacement(change.Mod, stabilityTolerance, version) is ModuleReplacement repl)
×
501
                        {
×
NEW
502
                            toRemove.Add(repl.ToReplace);
×
503
                            extraInstalls.Add(repl.ReplaceWith);
×
504
                        }
×
505
                        break;
×
506
                }
507
            }
2✔
508

509
            var installedModules = registry.InstalledModules
2✔
NEW
510
                                           .ToDictionary(imod => imod.Module.identifier,
×
NEW
511
                                                         imod => imod.Module);
×
512

513
            foreach (var dependent in registry.FindReverseDependencies(
6✔
NEW
514
                                          toRemove.Select(mod => mod.identifier)
×
515
                                                  .Except(toInstall.Select(m => m.identifier))
2✔
516
                                                  .ToList(),
517
                                          toInstall))
518
            {
×
519
                if (!changeSet.Any(ch => ch.ChangeType == GUIModChangeType.Replace
×
520
                                         && ch.Mod.identifier == dependent)
521
                    && installedModules.TryGetValue(dependent, out CkanModule? depMod)
522
                    && (registry.GetModuleByVersion(depMod.identifier, depMod.version)
523
                        ?? registry.InstalledModule(dependent)?.Module)
524
                        is CkanModule modByVer)
525
                {
×
526
                    changeSet.Add(new ModChange(modByVer, GUIModChangeType.Remove,
×
527
                                                new SelectionReason.DependencyRemoved(),
528
                                                coreConfig));
NEW
529
                    toRemove.Add(modByVer);
×
530
                }
×
531
            }
×
532

533
            foreach (var im in registry.FindRemovableAutoInstalled(
6✔
534
                InstalledAfterChanges(registry, changeSet).ToArray(),
535
                Array.Empty<CkanModule>(), game, stabilityTolerance, version))
536
            {
×
537
                changeSet.Add(new ModChange(im.Module, GUIModChangeType.Remove, new SelectionReason.NoLongerUsed(),
×
538
                                            coreConfig));
NEW
539
                toRemove.Add(im.Module);
×
540
            }
×
541

542
            // Get as many dependencies as we can, but leave decisions and prompts for installation time
543
            var resolver = new RelationshipResolver(toInstall, toRemove,
2✔
544
                                                    conflictOptions(stabilityTolerance),
545
                                                    registry, game, version);
546

547
            // Replace Install entries in changeset with the ones from resolver to get all the reasons
548
            return new Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>>(
2✔
549
                changeSet.Where(ch => !(ch.ChangeType is GUIModChangeType.Install
2✔
550
                                        // Leave in replacements
551
                                        && !ch.Reasons.Any(r => r is SelectionReason.Replacement)))
2✔
552
                         .OrderBy(ch => ch.Mod.identifier)
×
553
                         .Union(resolver.ModList()
554
                                        // Changeset already contains changes for these
555
                                        .Except(extraInstalls)
556
                                        .Select(m => new ModChange(m, GUIModChangeType.Install, resolver.ReasonsFor(m),
2✔
557
                                                                   coreConfig)))
558
                         .ToArray(),
559
                resolver.ConflictList,
560
                resolver.ConflictDescriptions.ToList());
561
        }
2✔
562

563
        /// <summary>
564
        /// Get the InstalledModules that we'll have after the changeset,
565
        /// not including dependencies
566
        /// </summary>
567
        /// <param name="registry">Registry with currently installed modules</param>
568
        /// <param name="changeSet">Changes to be made to the installed modules</param>
569
        private static IEnumerable<InstalledModule> InstalledAfterChanges(
570
            IRegistryQuerier               registry,
571
            IReadOnlyCollection<ModChange> changeSet)
572
        {
2✔
573
            var removingIdents = changeSet
2✔
574
                .Where(ch => ch.ChangeType != GUIModChangeType.Install)
2✔
NEW
575
                .Select(ch => ch.Mod.identifier)
×
576
                .ToHashSet();
577
            return registry.InstalledModules
2✔
NEW
578
                .Where(im => !removingIdents.Contains(im.identifier))
×
579
                .Concat(changeSet
580
                    .Where(ch => ch.ChangeType is not GUIModChangeType.Remove
2✔
581
                                              and not GUIModChangeType.Replace)
582
                    .Select(ch => new InstalledModule(
2✔
583
                        null,
584
                        (ch as ModUpgrade)?.targetMod ?? ch.Mod,
585
                        Enumerable.Empty<string>(),
586
                        false)));
587
        }
2✔
588

589
        private static RelationshipResolverOptions conflictOptions(StabilityToleranceConfig stabilityTolerance)
590
            => new RelationshipResolverOptions(stabilityTolerance)
2✔
591
            {
592
                without_toomanyprovides_kraken = true,
593
                proceed_with_inconsistencies   = true,
594
                without_enforce_consistency    = true,
595
                with_recommends                = false
596
            };
597

598
        #endregion
599

600
        #region Upgradeability
601

602
        /// <summary>
603
        /// Check upgradeability of all rows and set GUIMod.HasUpdate appropriately
604
        /// </summary>
605
        /// <param name="inst">Current game instance</param>
606
        /// <param name="registry">Current instance's registry</param>
607
        /// <param name="ChangeSet">Currently pending changeset</param>
608
        /// <param name="rows">The grid rows in case we need to replace some</param>
609
        /// <returns>true if any mod can be updated, false otherwise</returns>
610
        public bool ResetHasUpdate(GameInstance              inst,
611
                                   IRegistryQuerier          registry,
612
                                   List<ModChange>?          ChangeSet,
613
                                   DataGridViewRowCollection rows)
614
        {
2✔
615
            var upgGroups = registry.CheckUpgradeable(inst,
2✔
616
                                                      allLabels.HeldIdentifiers(inst)
617
                                                               .ToHashSet(),
618
                                                      allLabels.IgnoreMissingIdentifiers(inst)
619
                                                               .ToHashSet());
620
            var dlls = registry.InstalledDlls.ToList();
2✔
621
            foreach ((var upgradeable, var mods) in upgGroups)
8✔
622
            {
2✔
623
                foreach (var ident in mods.Select(m => m.identifier))
2✔
624
                {
2✔
625
                    dlls.Remove(ident);
2✔
626
                    CheckRowUpgradeable(inst, ChangeSet, rows, ident, upgradeable);
2✔
627
                }
2✔
628
            }
2✔
629
            // AD mods don't have CkanModules in the return value of CheckUpgradeable
630
            foreach (var ident in dlls)
6✔
631
            {
×
632
                CheckRowUpgradeable(inst, ChangeSet, rows, ident, false);
×
633
            }
×
634
            return upgGroups[true].Count > 0;
2✔
635
        }
2✔
636

637
        private void CheckRowUpgradeable(GameInstance              inst,
638
                                         List<ModChange>?          ChangeSet,
639
                                         DataGridViewRowCollection rows,
640
                                         string                    ident,
641
                                         bool                      upgradeable)
642
        {
2✔
643
            if (full_list_of_mod_rows.TryGetValue(ident, out DataGridViewRow? row)
2✔
644
                && row.Tag is GUIMod gmod
645
                && gmod.HasUpdate != upgradeable)
646
            {
×
647
                gmod.HasUpdate = upgradeable;
×
648
                if (row.Visible)
×
649
                {
×
650
                    // Swap whether the row has an upgrade checkbox
651
                    var newRow = full_list_of_mod_rows[ident] = MakeRow(gmod, ChangeSet, inst);
×
652
                    var rowIndex = row.Index;
×
653
                    var selected = row.Selected;
×
654
                    rows.Remove(row);
×
655
                    rows.Insert(rowIndex, newRow);
×
656
                    if (selected)
×
657
                    {
×
658
                        rows[rowIndex].Selected = true;
×
659
                    }
×
660
                }
×
661
            }
×
662
        }
2✔
663

664
        #endregion
665

666
        private static Color SelectionBlend(Color c)
667
            => c == Color.Empty
2✔
668
                ? SystemColors.Highlight
669
                : SystemColors.Highlight.AlphaBlendWith(selectionAlpha, c);
670

671
        private const float selectionAlpha = 0.4f;
672

673
        private readonly ModuleLabelList                allLabels;
674
        private readonly ModuleTagList                  allTags;
675
        private readonly IConfiguration                 coreConfig;
676
        private readonly GUIConfiguration               guiConfig;
677
        private          IReadOnlyCollection<ModSearch> activeSearches;
678

679
        private static readonly Color conflictColor = Color.FromArgb(255, 64, 64);
2✔
680

681
        private static readonly ILog log = LogManager.GetLogger(typeof(ModList));
2✔
682
    }
683
}
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