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

KSP-CKAN / CKAN / 17904669173

22 Sep 2025 04:34AM UTC coverage: 75.604% (+1.2%) from 74.397%
17904669173

push

github

HebaruSan
Merge #4443 Report number of filtered files in install

5231 of 7236 branches covered (72.29%)

Branch coverage included in aggregate %.

192 of 218 new or added lines in 41 files covered. (88.07%)

35 existing lines in 7 files now uncovered.

11163 of 14448 relevant lines covered (77.26%)

1.58 hits per line

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

86.63
/GUI/Model/ModSearch.cs
1
using System;
2
using System.Linq;
3
using System.Collections.Generic;
4
#if NET5_0_OR_GREATER
5
using System.Runtime.Versioning;
6
#endif
7

8
namespace CKAN.GUI
9
{
10
    /// <summary>
11
    /// An object representing a search to be performed of the mod list.
12
    /// Create one with the constructor or Parse(), and use it with Matches().
13
    /// </summary>
14
    #if NET5_0_OR_GREATER
15
    [SupportedOSPlatform("windows")]
16
    #endif
17
    public sealed class ModSearch : IEquatable<ModSearch>
18
    {
19
        /// <summary>
20
        /// Initialize a mod search object.
21
        /// Null or empty parameters are treated as matching everything.
22
        /// </summary>
23
        /// <param name="allLabels">List of labels to be searched</param>
24
        /// <param name="instance">Game instance for finding labels to search</param>
25
        /// <param name="byName">String to search for in mod names, identifiers, and abbreviations</param>
26
        /// <param name="byAuthors">String to search for in author names</param>
27
        /// <param name="byDescription">String to search for in mod descriptions</param>
28
        /// <param name="licenses">Licenses to search for</param>
29
        /// <param name="localizations">Languages to search for in mod localizations</param>
30
        /// <param name="depends">Identifier prefix to find in mod depends relationships</param>
31
        /// <param name="recommends">Identifier prefix to find in mod recommends relationships</param>
32
        /// <param name="suggests">Identifier prefix to find in mod suggests relationships</param>
33
        /// <param name="conflicts">Identifier prefix to find in mod conflicts relationships</param>
34
        /// <param name="supports">Identifier prefix to find in mod supports relationships</param>
35
        /// <param name="tagNames">Names of tags to search for</param>
36
        /// <param name="labelNames">Names of labels to search for</param>
37
        /// <param name="compatible">True to find only compatible mods, false to find only incompatible, null for both</param>
38
        /// <param name="installed">True to find only installed mods, false to find only non-installed, null for both</param>
39
        /// <param name="cached">True to find only cached mods, false to find only uncached, null for both</param>
40
        /// <param name="newlyCompatible">True to find only newly compatible mods, false to find only non newly compatible, null for both</param>
41
        /// <param name="upgradeable">True to find only upgradeable mods, false to find only non-upgradeable, null for both</param>
42
        /// <param name="replaceable">True to find only replaceable mods, false to find only non-replaceable, null for both</param>
43
        /// <param name="combined">Full formatted search string if known, will be auto generated otherwise</param>
44
        public ModSearch(
2✔
45
            ModuleLabelList allLabels,
46
            GameInstance instance,
47
            string byName, List<string> byAuthors, string byDescription,
48
            List<string>? licenses, List<string>? localizations,
49
            List<string>? depends, List<string>? recommends, List<string>? suggests, List<string>? conflicts,
50
            List<string>? supports,
51
            List<string>? tagNames, List<string>? labelNames,
52
            bool? compatible, bool? installed, bool? cached, bool? newlyCompatible,
53
            bool? upgradeable, bool? replaceable,
54
            string? combined = null)
55
        {
2✔
56
            Instance = instance;
2✔
57
            Name = (ShouldNegateTerm(byName, out string subName) ? "-" : "")
2✔
58
                + CkanModule.nonAlphaNums.Replace(subName, "");
59
            initStringList(Authors, byAuthors);
2✔
60
            Description = (ShouldNegateTerm(byDescription, out string subDesc) ? "-" : "")
2✔
61
                + CkanModule.nonAlphaNums.Replace(subDesc, "");
62
            initStringList(Localizations, localizations);
2✔
63
            initStringList(Licenses,      licenses);
2✔
64

65
            initStringList(DependsOn,     depends);
2✔
66
            initStringList(Recommends,    recommends);
2✔
67
            initStringList(Suggests,      suggests);
2✔
68
            initStringList(ConflictsWith, conflicts);
2✔
69
            initStringList(Supports,      supports);
2✔
70

71
            initStringList(TagNames,   tagNames);
2✔
72
            initStringList(LabelNames, labelNames);
2✔
73
            LabelsByNegation = FindLabels(allLabels, instance, LabelNames);
2✔
74

75
            Compatible      = compatible;
2✔
76
            Installed       = installed;
2✔
77
            Cached          = cached;
2✔
78
            NewlyCompatible = newlyCompatible;
2✔
79
            Upgradeable     = upgradeable;
2✔
80
            Replaceable     = replaceable;
2✔
81

82
            Combined = combined ?? getCombined();
2✔
83
        }
2✔
84

85
        private static void initStringList(List<string> dest, List<string>? source)
86
        {
2✔
87
            if (source?.Any() ?? false)
2✔
88
            {
2✔
89
                dest.AddRange(source);
2✔
90
            }
2✔
91
        }
2✔
92

93
        public ModSearch(ModuleLabelList allLabels,
2✔
94
                         GameInstance instance,
95
                         GUIModFilter filter,
96
                         ModuleTag?   tag   = null,
97
                         ModuleLabel? label = null)
98
        {
2✔
99
            Instance = instance;
2✔
100
            switch (filter)
2!
101
            {
102
                case GUIModFilter.Compatible:               Compatible      = true;            break;
3✔
103
                case GUIModFilter.Incompatible:             Compatible      = false;           break;
3✔
104
                case GUIModFilter.Installed:                Installed       = true;            break;
3✔
105
                case GUIModFilter.NotInstalled:             Installed       = false;           break;
3✔
106
                case GUIModFilter.InstalledUpdateAvailable: Upgradeable     = true;            break;
3✔
107
                case GUIModFilter.Replaceable:              Replaceable     = true;            break;
3✔
108
                case GUIModFilter.Cached:                   Cached          = true;            break;
3✔
109
                case GUIModFilter.Uncached:                 Cached          = false;           break;
3✔
110
                case GUIModFilter.NewInRepository:          NewlyCompatible = true;            break;
3✔
111
                case GUIModFilter.Tag:                      TagNames.Add(tag?.Name ?? "");     break;
3!
112
                case GUIModFilter.CustomLabel:              LabelNames.Add(label?.Name ?? ""); break;
3✔
113
                default:
114
                case GUIModFilter.All:
115
                    break;
2✔
116
            }
117
            LabelsByNegation = FindLabels(allLabels, instance, LabelNames);
2✔
118
            Combined = getCombined();
2✔
119
        }
2✔
120

121
        private readonly GameInstance Instance;
122

123
        /// <summary>
124
        /// String to search for in mod names, identifiers, and abbreviations
125
        /// </summary>
126
        public readonly string Name = "";
2✔
127

128
        /// <summary>
129
        /// String to search for in author names
130
        /// </summary>
131
        public readonly List<string> Authors = new List<string>();
2✔
132

133
        /// <summary>
134
        /// String to search for in mod descriptions
135
        /// </summary>
136
        public readonly string Description = "";
2✔
137

138
        /// <summary>
139
        /// License substring to search for in mod licenses
140
        /// </summary>
141
        public readonly List<string> Licenses = new List<string>();
2✔
142

143
        /// <summary>
144
        /// Language to search for in mod localizations
145
        /// </summary>
146
        public readonly List<string> Localizations = new List<string>();
2✔
147

148
        /// <summary>
149
        /// Identifier prefix to find in mod depends relationships
150
        /// </summary>
151
        public readonly List<string> DependsOn = new List<string>();
2✔
152

153
        /// <summary>
154
        /// Identifier prefix to find in mod recommends relationships
155
        /// </summary>
156
        public readonly List<string> Recommends = new List<string>();
2✔
157

158
        /// <summary>
159
        /// Identifier prefix to find in mod suggests relationships
160
        /// </summary>
161
        public readonly List<string> Suggests = new List<string>();
2✔
162

163
        /// <summary>
164
        /// Identifier prefix to find in mod conflicts relationships
165
        /// </summary>
166
        public readonly List<string> ConflictsWith = new List<string>();
2✔
167

168
        /// <summary>
169
        /// Identifier prefix to find in mod conflicts relationships
170
        /// </summary>
171
        public readonly List<string> Supports = new List<string>();
2✔
172

173
        /// <summary>
174
        /// Full formatted search string
175
        /// </summary>
176
        public readonly string? Combined;
177

178
        public readonly List<string> TagNames   = new List<string>();
2✔
179
        public readonly List<string> LabelNames = new List<string>();
2✔
180
        private readonly IDictionary<(bool negate, bool exclude), ModuleLabel[]> LabelsByNegation;
181

182
        public readonly bool? Compatible;
183
        public readonly bool? Installed;
184
        public readonly bool? Cached;
185
        public readonly bool? NewlyCompatible;
186
        public readonly bool? Upgradeable;
187
        public readonly bool? Replaceable;
188

189
        /// <summary>
190
        /// Create a new search from a list of authors
191
        /// </summary>
192
        /// <param name="allLabels">List of labels for searching</param>
193
        /// <param name="instance">Game instance for searching</param>
194
        /// <param name="authors">The authors for the search</param>
195
        /// <returns>A search for the authors</returns>
196
        public static ModSearch FromAuthors(ModuleLabelList     allLabels,
197
                                            GameInstance        instance,
198
                                            IEnumerable<string> authors)
199
            => new ModSearch(
2✔
200
                allLabels,
201
                instance,
202
                // Can't search for spaces, so massage them like SearchableAuthors
203
                "", authors.Select(a => CkanModule.nonAlphaNums.Replace(a, "")).ToList(), "",
2✔
204
                null, null,
205
                null, null, null, null, null,
206
                null, null,
207
                null, null, null, null,
208
                null, null);
209

210
        /// <summary>
211
        /// Create a new search by merging this and another
212
        /// </summary>
213
        /// <param name="allLabels">List of labels for searching</param>
214
        /// <param name="other">The other search for merging</param>
215
        /// <returns>A search containing all the search terms</returns>
216
        public ModSearch MergedWith(ModuleLabelList allLabels,
217
                                    ModSearch       other)
218
            => new ModSearch(
2✔
219
                allLabels,
220
                Instance,
221
                Name + other.Name,
222
                Authors.Concat(other.Authors).Distinct().ToList(),
223
                Description + other.Description,
224
                Licenses.Concat(other.Licenses).Distinct().ToList(),
225
                Localizations.Concat(other.Localizations).Distinct().ToList(),
226
                DependsOn.Concat(other.DependsOn).Distinct().ToList(),
227
                Recommends.Concat(other.Recommends).Distinct().ToList(),
228
                Suggests.Concat(other.Suggests).Distinct().ToList(),
229
                ConflictsWith.Concat(other.ConflictsWith).Distinct().ToList(),
230
                Supports.Concat(other.Supports).Distinct().ToList(),
231
                TagNames.Concat(other.TagNames).Distinct().ToList(),
232
                LabelNames.Concat(other.LabelNames).Distinct().ToList(),
233
                Compatible      ?? other.Compatible,
234
                Installed       ?? other.Installed,
235
                Cached          ?? other.Cached,
236
                NewlyCompatible ?? other.NewlyCompatible,
237
                Upgradeable     ?? other.Upgradeable,
238
                Replaceable     ?? other.Replaceable);
239

240
        /// <summary>
241
        /// Generate a full formatted search string from the parameters.
242
        /// MUST be the inverse of Parse!
243
        /// </summary>
244
        /// <returns>
245
        ///
246
        /// </returns>
247
        private string? getCombined()
248
        {
2✔
249
            var pieces = new List<string>();
2✔
250
            if (!string.IsNullOrWhiteSpace(Name))
2✔
251
            {
2✔
252
                pieces.Add(Name);
2✔
253
            }
2✔
254
            foreach (var author in Authors.Where(auth => !string.IsNullOrEmpty(auth)))
2✔
255
            {
2✔
256
                pieces.Add(AddTermPrefix("@", author));
2✔
257
            }
2✔
258
            if (!string.IsNullOrWhiteSpace(Description))
2✔
259
            {
2✔
260
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchDescriptionPrefix, Description));
2✔
261
            }
2✔
262
            foreach (var license in Licenses.Where(lic => !string.IsNullOrEmpty(lic)))
2✔
263
            {
2✔
264
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchLicensePrefix, license));
2✔
265
            }
2✔
266
            foreach (var localization in Localizations.Where(lang => !string.IsNullOrEmpty(lang)))
2✔
267
            {
2✔
268
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchLanguagePrefix, localization));
2✔
269
            }
2✔
270
            foreach (var dep in DependsOn.Where(d => !string.IsNullOrEmpty(d)))
2✔
271
            {
2✔
272
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchDependsPrefix, dep));
2✔
273
            }
2✔
274
            foreach (var rec in Recommends.Where(r => !string.IsNullOrEmpty(r)))
2✔
275
            {
2✔
276
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchRecommendsPrefix, rec));
2✔
277
            }
2✔
278
            foreach (var sug in Suggests.Where(s => !string.IsNullOrEmpty(s)))
2✔
279
            {
2✔
280
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchSuggestsPrefix, sug));
2✔
281
            }
2✔
282
            foreach (var conf in ConflictsWith.Where(c => !string.IsNullOrEmpty(c)))
2✔
283
            {
2✔
284
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchConflictsPrefix, conf));
2✔
285
            }
2✔
286
            foreach (var sup in Supports.Where(s => !string.IsNullOrEmpty(s)))
2✔
287
            {
2✔
288
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchSupportsPrefix, sup));
2✔
289
            }
2✔
290
            foreach (var tagName in TagNames)
5✔
291
            {
2✔
292
                pieces.Add(AddTermPrefix(Properties.Resources.ModSearchTagPrefix, tagName ?? ""));
2✔
293
            }
2✔
294
            foreach (var label in LabelNames)
5✔
295
            {
2✔
296
                pieces.Add($"{Properties.Resources.ModSearchLabelPrefix}{label.Replace(" ", "")}");
2✔
297
            }
2✔
298
            if (Compatible.HasValue)
2✔
299
            {
2✔
300
                pieces.Add(triStateString(Compatible.Value, Properties.Resources.ModSearchCompatibleSuffix));
2✔
301
            }
2✔
302
            if (Installed.HasValue)
2✔
303
            {
2✔
304
                pieces.Add(triStateString(Installed.Value, Properties.Resources.ModSearchInstalledSuffix));
2✔
305
            }
2✔
306
            if (Cached.HasValue)
2✔
307
            {
2✔
308
                pieces.Add(triStateString(Cached.Value, Properties.Resources.ModSearchCachedSuffix));
2✔
309
            }
2✔
310
            if (NewlyCompatible.HasValue)
2✔
311
            {
2✔
312
                pieces.Add(triStateString(NewlyCompatible.Value, Properties.Resources.ModSearchNewlyCompatibleSuffix));
2✔
313
            }
2✔
314
            if (Upgradeable.HasValue)
2✔
315
            {
2✔
316
                pieces.Add(triStateString(Upgradeable.Value, Properties.Resources.ModSearchUpgradeableSuffix));
2✔
317
            }
2✔
318
            if (Replaceable.HasValue)
2✔
319
            {
2✔
320
                pieces.Add(triStateString(Replaceable.Value, Properties.Resources.ModSearchReplaceableSuffix));
2✔
321
            }
2✔
322
            return pieces.Count == 0
2✔
323
                ? null
324
                : string.Join(" ", pieces);
325
        }
2✔
326

327
        private static string triStateString(bool val, string suffix)
328
        {
2✔
329
            string prefix = val
2✔
330
                ? Properties.Resources.ModSearchYesPrefix
331
                : Properties.Resources.ModSearchNoPrefix;
332
            return prefix + suffix;
2✔
333
        }
2✔
334

335
        /// <summary>
336
        /// Parse a full formatted search string into its component parts.
337
        /// May throw a Kraken if the syntax is bad.
338
        /// MUST be the inverse of getCombined!
339
        /// </summary>
340
        /// <param name="allLabels">List of labels for searching</param>
341
        /// <param name="instance">Game instance for searching</param>
342
        /// <param name="combined">Full formatted search string</param>
343
        /// <returns>
344
        /// New search object, or null if no search terms defined
345
        /// </returns>
346
        public static ModSearch? Parse(ModuleLabelList allLabels,
347
                                       GameInstance    instance,
348
                                       string          combined)
349
        {
2✔
350
            if (string.IsNullOrWhiteSpace(combined))
2✔
351
            {
2✔
352
                return null;
2✔
353
            }
354
            string byName          = "";
2✔
355
            var    byAuthors       = new List<string>();
2✔
356
            string byDescription   = "";
2✔
357
            var    byLicenses      = new List<string>();
2✔
358
            var    byLocalizations = new List<string>();
2✔
359

360
            var depends    = new List<string>();
2✔
361
            var recommends = new List<string>();
2✔
362
            var suggests   = new List<string>();
2✔
363
            var conflicts  = new List<string>();
2✔
364
            var supports   = new List<string>();
2✔
365

366
            List<string> tagNames = new List<string>();
2✔
367
            List<string> labels   = new List<string>();
2✔
368

369
            bool? compatible      = null;
2✔
370
            bool? installed       = null;
2✔
371
            bool? cached          = null;
2✔
372
            bool? newlyCompatible = null;
2✔
373
            bool? upgradeable     = null;
2✔
374
            bool? replaceable     = null;
2✔
375

376
            var pieces = combined.Split();
2✔
377
            foreach (string s in pieces)
5✔
378
            {
2✔
379
                if (TryPrefix(s, "@", out string auth))
2✔
380
                {
2✔
381
                    byAuthors.Add(auth);
2✔
382
                }
2✔
383
                else if (TryPrefix(s, Properties.Resources.ModSearchDescriptionPrefix, out string desc))
2✔
384
                {
2✔
385
                    byDescription += (ShouldNegateTerm(desc, out string subDesc) ? "-" : "") + CkanModule.nonAlphaNums.Replace(subDesc, "");
2✔
386
                }
2✔
387
                else if (TryPrefix(s, Properties.Resources.ModSearchLicensePrefix, out string lic))
2✔
388
                {
2✔
389
                    byLicenses.Add(lic);
2✔
390
                }
2✔
391
                else if (TryPrefix(s, Properties.Resources.ModSearchLanguagePrefix, out string lang))
2✔
392
                {
2✔
393
                    byLocalizations.Add(lang);
2✔
394
                }
2✔
395
                else if (TryPrefix(s, Properties.Resources.ModSearchDependsPrefix, out string dep))
2✔
396
                {
2✔
397
                    depends.Add(dep);
2✔
398
                }
2✔
399
                else if (TryPrefix(s, Properties.Resources.ModSearchRecommendsPrefix, out string rec))
2✔
400
                {
2✔
401
                    recommends.Add(rec);
2✔
402
                }
2✔
403
                else if (TryPrefix(s, Properties.Resources.ModSearchSuggestsPrefix, out string sug))
2✔
404
                {
2✔
405
                    suggests.Add(sug);
2✔
406
                }
2✔
407
                else if (TryPrefix(s, Properties.Resources.ModSearchConflictsPrefix, out string conf))
2✔
408
                {
2✔
409
                    conflicts.Add(conf);
2✔
410
                }
2✔
411
                else if (TryPrefix(s, Properties.Resources.ModSearchSupportsPrefix, out string sup))
2✔
412
                {
2✔
413
                    supports.Add(sup);
2✔
414
                }
2✔
415
                else if (TryPrefix(s, Properties.Resources.ModSearchTagPrefix, out string tagName))
2✔
416
                {
2✔
417
                    tagNames.Add(tagName);
2✔
418
                }
2✔
419
                else if (TryPrefix(s, Properties.Resources.ModSearchLabelPrefix, out string labelName))
2✔
420
                {
2✔
421
                    labels.Add(labelName);
2✔
422
                }
2✔
423
                else if (TryPrefix(s, Properties.Resources.ModSearchYesPrefix, out string yesSuffix))
2✔
424
                {
2✔
425
                    if (yesSuffix == Properties.Resources.ModSearchCompatibleSuffix)
2✔
426
                    {
2✔
427
                        compatible = true;
2✔
428
                    }
2✔
429
                    else if (yesSuffix == Properties.Resources.ModSearchInstalledSuffix)
2✔
430
                    {
2✔
431
                        installed = true;
2✔
432
                    }
2✔
433
                    else if (yesSuffix == Properties.Resources.ModSearchCachedSuffix)
2!
434
                    {
×
435
                        cached = true;
×
436
                    }
×
437
                    else if (yesSuffix == Properties.Resources.ModSearchNewlyCompatibleSuffix)
2!
438
                    {
×
439
                        newlyCompatible = true;
×
440
                    }
×
441
                    else if (yesSuffix == Properties.Resources.ModSearchUpgradeableSuffix)
2!
442
                    {
×
443
                        upgradeable = true;
×
444
                    }
×
445
                    else if (yesSuffix == Properties.Resources.ModSearchReplaceableSuffix)
2!
446
                    {
2✔
447
                        replaceable = true;
2✔
448
                    }
2✔
449
                }
2✔
450
                else if (TryPrefix(s, Properties.Resources.ModSearchNoPrefix, out string noSuffix))
2✔
451
                {
2✔
452
                    if (noSuffix == Properties.Resources.ModSearchCompatibleSuffix)
2!
453
                    {
×
454
                        compatible = false;
×
455
                    }
×
456
                    else if (noSuffix == Properties.Resources.ModSearchInstalledSuffix)
2!
457
                    {
×
458
                        installed = false;
×
459
                    }
×
460
                    else if (noSuffix == Properties.Resources.ModSearchCachedSuffix)
2✔
461
                    {
2✔
462
                        cached = false;
2✔
463
                    }
2✔
464
                    else if (noSuffix == Properties.Resources.ModSearchNewlyCompatibleSuffix)
2!
465
                    {
×
466
                        newlyCompatible = false;
×
467
                    }
×
468
                    else if (noSuffix == Properties.Resources.ModSearchUpgradeableSuffix)
2!
469
                    {
×
470
                        upgradeable = false;
×
471
                    }
×
472
                    else if (noSuffix == Properties.Resources.ModSearchReplaceableSuffix)
2!
473
                    {
2✔
474
                        replaceable = false;
2✔
475
                    }
2✔
476
                }
2✔
477
                else
478
                {
2✔
479
                    // No special format = search names and identifiers
480
                    byName += (ShouldNegateTerm(s, out string subS) ? "-" : "") + CkanModule.nonAlphaNums.Replace(subS, "");
2✔
481
                }
2✔
482
            }
2✔
483
            return new ModSearch(
2✔
484
                allLabels,
485
                instance,
486
                byName, byAuthors, byDescription,
487
                byLicenses, byLocalizations,
488
                depends, recommends, suggests, conflicts, supports,
489
                tagNames, labels,
490
                compatible, installed, cached, newlyCompatible,
491
                upgradeable, replaceable,
492
                combined
493
            );
494
        }
2✔
495

496
        private static bool TryPrefix(string container, string prefix, out string remainder)
497
        {
2✔
498
            if (container.StartsWith(prefix))
2✔
499
            {
2✔
500
                remainder = container[prefix.Length..];
2✔
501
                return true;
2✔
502
            }
503
            else if (container.StartsWith($"-{prefix}"))
2!
504
            {
×
505
                remainder = $"-{container[(prefix.Length + 1)..]}";
×
506
                return true;
×
507
            }
508
            else
509
            {
2✔
510
                remainder = "";
2✔
511
                return false;
2✔
512
            }
513
        }
2✔
514

515
        private static string AddTermPrefix(string prefix, string term)
516
            => term.StartsWith("-")
2!
517
                ? $"-{prefix}{term[1..]}"
518
                : $"{prefix}{term}";
519

520
        private static bool ShouldNegateTerm(string term, out string subTerm)
521
        {
2✔
522
            if (term.StartsWith("-"))
2!
523
            {
×
524
                subTerm = term[1..];
×
525
                return true;
×
526
            }
527
            else
528
            {
2✔
529
                subTerm = term;
2✔
530
                return false;
2✔
531
            }
532
        }
2✔
533

534
        /// <summary>
535
        /// Check whether our filters match the given mod.
536
        /// </summary>
537
        /// <param name="mod">Mod to check</param>
538
        /// <returns>
539
        /// true if mod matches filters, false otherwise
540
        /// </returns>
541
        public bool Matches(GUIMod mod)
542
            => MatchesName(mod)
2!
543
                && MatchesAuthors(mod)
544
                && MatchesDescription(mod)
545
                && MatchesLicenses(mod)
546
                && MatchesLocalizations(mod)
547
                && MatchesDepends(mod)
548
                && MatchesRecommends(mod)
549
                && MatchesSuggests(mod)
550
                && MatchesConflicts(mod)
551
                && MatchesSupports(mod)
552
                && MatchesTags(mod)
553
                && MatchesLabels(mod)
554
                && MatchesCompatible(mod)
555
                && MatchesInstalled(mod)
556
                && MatchesCached(mod)
557
                && MatchesNewlyCompatible(mod)
558
                && MatchesUpgradeable(mod)
559
                && MatchesReplaceable(mod);
560

561
        private bool MatchesName(GUIMod mod)
562
            => string.IsNullOrWhiteSpace(Name)
2!
563
                || ShouldNegateTerm(Name, out string subName) ^ (
564
                    mod.Abbrevation.IndexOf(subName, StringComparison.InvariantCultureIgnoreCase) != -1
565
                    || mod.SearchableName.IndexOf(subName, StringComparison.InvariantCultureIgnoreCase) != -1
566
                    || mod.SearchableIdentifier.IndexOf(subName, StringComparison.InvariantCultureIgnoreCase) != -1);
567

568
        private bool MatchesAuthors(GUIMod mod)
569
            => Authors.Count < 1
2!
570
                || Authors.All(searchAuth =>
571
                    ShouldNegateTerm(searchAuth, out string subAuth)
×
572
                    ^ mod.SearchableAuthors.Any(modAuth =>
573
                            modAuth.StartsWith(subAuth, StringComparison.InvariantCultureIgnoreCase)));
×
574

575
        private bool MatchesDescription(GUIMod mod)
576
            => string.IsNullOrWhiteSpace(Description)
2!
577
                || ShouldNegateTerm(Description, out string subDesc) ^ (
578
                    mod.SearchableAbstract.IndexOf(subDesc, StringComparison.InvariantCultureIgnoreCase) != -1
579
                    || mod.SearchableDescription.IndexOf(subDesc, StringComparison.InvariantCultureIgnoreCase) != -1);
580

581
        private bool MatchesLicenses(GUIMod mod)
582
        {
2✔
583
            var ckm = mod.Module;
2✔
584
            return Licenses.Count < 1
2!
585
                || (
586
                    ckm.license != null
587
                    && Licenses.All(searchLic =>
588
                        ShouldNegateTerm(searchLic, out string subLic)
×
589
                        ^ ckm.license.Any(modLic =>
590
                            modLic.ToString().StartsWith(subLic, StringComparison.InvariantCultureIgnoreCase)))
×
591
                );
592
        }
2✔
593

594
        private bool MatchesLocalizations(GUIMod mod)
595
        {
2✔
596
            var ckm = mod.Module;
2✔
597
            return Localizations.Count < 1
2!
598
                || (
599
                    ckm.localizations != null
600
                    && Localizations.All(searchLoc =>
601
                        ShouldNegateTerm(searchLoc, out string subLoc)
×
602
                        ^ ckm.localizations.Any(modLoc =>
603
                            modLoc.StartsWith(subLoc, StringComparison.InvariantCultureIgnoreCase)))
×
604
                );
605
        }
2✔
606

607
        private bool MatchesDepends(GUIMod mod)
608
            => RelationshipMatch(mod.Module.depends, DependsOn);
2✔
609
        private bool MatchesRecommends(GUIMod mod)
610
            => RelationshipMatch(mod.Module.recommends, Recommends);
2✔
611
        private bool MatchesSuggests(GUIMod mod)
612
            => RelationshipMatch(mod.Module.suggests, Suggests);
2✔
613
        private bool MatchesConflicts(GUIMod mod)
614
            => RelationshipMatch(mod.Module.conflicts, ConflictsWith);
2✔
615
        private bool MatchesSupports(GUIMod mod)
616
            => RelationshipMatch(mod.Module.supports, Supports);
2✔
617
        private static bool RelationshipMatch(List<RelationshipDescriptor>? rels, List<string> toFind)
618
            => toFind.Count < 1
2!
619
                || (rels != null && toFind.All(searchRel =>
620
                    ShouldNegateTerm(searchRel, out string subRel)
×
621
                    ^ rels.Any(r => r.StartsWith(subRel))));
×
622

623
        private bool MatchesTags(GUIMod mod)
624
            => TagNames.Count < 1
2!
625
                || TagNames.All(tn =>
626
                        ShouldNegateTerm(tn, out string subTag) ^ (
×
627
                            string.IsNullOrEmpty(subTag)
628
                                ? mod.Module.Tags == null
629
                                : mod.Module.Tags?.Contains(subTag) ?? false));
630

631
        private bool MatchesLabels(GUIMod mod)
632
            => LabelsByNegation.All(kvp =>
2✔
633
                   kvp.Key.negate ^ (
×
634
                       kvp.Key.exclude
NEW
635
                           ? kvp.Value.All(lbl => !lbl.ContainsModule(Instance.Game,
×
636
                                                                      mod.Identifier))
637
                           : kvp.Value.Length > 0
NEW
638
                                 && kvp.Value.All(lbl => lbl.ContainsModule(Instance.Game,
×
639
                                                                            mod.Identifier))));
640

641
        private bool MatchesCompatible(GUIMod mod)
642
            => !Compatible.HasValue || Compatible.Value == !mod.IsIncompatible;
2!
643

644
        private bool MatchesInstalled(GUIMod mod)
645
            => !Installed.HasValue || Installed.Value == mod.IsInstalled;
2!
646

647
        private bool MatchesCached(GUIMod mod)
648
            => !Cached.HasValue || Cached.Value == mod.IsCached;
2!
649

650
        private bool MatchesNewlyCompatible(GUIMod mod)
651
            => !NewlyCompatible.HasValue || NewlyCompatible.Value == mod.IsNew;
2!
652

653
        private bool MatchesUpgradeable(GUIMod mod)
654
            => !Upgradeable.HasValue || Upgradeable.Value == mod.HasUpdate;
2!
655

656
        private bool MatchesReplaceable(GUIMod mod)
657
            => !Replaceable.HasValue || Replaceable.Value == (mod.IsInstalled && mod.HasReplacement);
2✔
658

659
        private static IDictionary<(bool negate, bool exclude), ModuleLabel[]> FindLabels(
660
                ModuleLabelList     allLabels,
661
                GameInstance        inst,
662
                IEnumerable<string> names)
663
            => FindLabels(allLabels.LabelsFor(inst.Name).ToArray(), names);
2✔
664

665
        private static IDictionary<(bool negate, bool exclude), ModuleLabel[]> FindLabels(
666
                ModuleLabel[]       instLabels,
667
                IEnumerable<string> names)
668
            => names.Select(ln => (negate:  ShouldNegateTerm(ln.Replace(" ", ""),
2✔
669
                                                             out string subLabel),
670
                                   exclude: string.IsNullOrEmpty(subLabel),
671
                                   labels:  string.IsNullOrEmpty(subLabel)
672
                                                ? instLabels
673
                                                : instLabels.Where(lbl => lbl.Name
2✔
674
                                                                             .Replace(" ", "")
675
                                                                             .Equals(subLabel,
676
                                                                                     StringComparison.OrdinalIgnoreCase))
677
                                                            .ToArray()))
678
                    .GroupBy(tuple => (tuple.negate, tuple.exclude),
2✔
679
                             tuple => tuple.labels)
2✔
680
                    .ToDictionary(grp => grp.Key,
2✔
681
                                  grp => grp.Any(labels => labels.Length < 1)
2!
682
                                             // A name that matches no labels makes nothing match
683
                                             ? Array.Empty<ModuleLabel>()
684
                                             : grp.SelectMany(labels => labels)
2✔
685
                                                       .Distinct()
686
                                                       .ToArray());
687

688
        public bool Equals(ModSearch? other)
689
            => other != null
2!
690
                && Name            == other.Name
691
                && Description     == other.Description
692
                && Compatible      == other.Compatible
693
                && Installed       == other.Installed
694
                && Cached          == other.Cached
695
                && NewlyCompatible == other.NewlyCompatible
696
                && Upgradeable     == other.Upgradeable
697
                && Replaceable     == other.Replaceable
698
                && Authors.SequenceEqual(other.Authors)
699
                && Licenses.SequenceEqual(other.Licenses)
700
                && Localizations.SequenceEqual(other.Localizations)
701
                && DependsOn.SequenceEqual(other.DependsOn)
702
                && Recommends.SequenceEqual(other.Recommends)
703
                && Suggests.SequenceEqual(other.Suggests)
704
                && ConflictsWith.SequenceEqual(other.ConflictsWith)
705
                && Supports.SequenceEqual(other.Supports)
706
                && TagNames.SequenceEqual(other.TagNames)
707
                && LabelNames.SequenceEqual(other.LabelNames);
708

709
        public override bool Equals(object? obj)
710
            => Equals(obj as ModSearch);
×
711

712
        public override int GetHashCode()
713
            => Combined?.GetHashCode() ?? 0;
2!
714

715
    }
716
}
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

© 2025 Coveralls, Inc