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

KSP-CKAN / CKAN / 17865799316

19 Sep 2025 05:47PM UTC coverage: 74.397% (+0.2%) from 74.179%
17865799316

Pull #4441

github

web-flow
Merge 8a2699706 into e49348427
Pull Request #4441: Don't clone with symlinks on Linux

5140 of 7262 branches covered (70.78%)

Branch coverage included in aggregate %.

5 of 8 new or added lines in 2 files covered. (62.5%)

11 existing lines in 4 files now uncovered.

11060 of 14513 relevant lines covered (76.21%)

1.56 hits per line

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

89.4
/Core/Versioning/GameVersion.cs
1
using System;
2
using System.Linq;
3
using System.Text;
4
using System.Text.RegularExpressions;
5
using System.Collections.Generic;
6
using System.Diagnostics.CodeAnalysis;
7

8
using Newtonsoft.Json;
9
using Newtonsoft.Json.Linq;
10
using log4net;
11

12
using CKAN.Games;
13

14
namespace CKAN.Versioning
15
{
16
    /// <summary>
17
    /// Represents the version number of a Kerbal Space Program (KSP) installation.
18
    /// </summary>
19
    [JsonConverter(typeof(GameVersionJsonConverter))]
20
    public sealed partial class GameVersion
21
    {
22
        private static readonly Regex Pattern = new Regex(
2✔
23
            @"^(?<major>\d+)(?:\.(?<minor>\d+)(?:\.(?<patch>\d+)(?:\.(?<build>\d+))?)?)?$",
24
            RegexOptions.Compiled);
25

26
        private const int Undefined = -1;
27

28
        public static readonly GameVersion Any = new GameVersion();
2✔
29

30
        private readonly int _major;
31
        private readonly int _minor;
32
        private readonly int _patch;
33
        private readonly int _build;
34

35
        private readonly string? _string;
36

37
        /// <summary>
38
        /// Gets the value of the major component of the version number for the current <see cref="GameVersion"/>
39
        /// object.
40
        /// </summary>
41
        public int Major => _major;
2✔
42

43
        /// <summary>
44
        /// Gets the value of the minor component of the version number for the current <see cref="GameVersion"/>
45
        /// object.
46
        /// </summary>
47
        public int Minor => _minor;
2✔
48

49
        /// <summary>
50
        /// Gets the value of the patch component of the version number for the current <see cref="GameVersion"/>
51
        /// object.
52
        /// </summary>
53
        public int Patch => _patch;
2✔
54

55
        /// <summary>
56
        /// Gets the value of the build component of the version number for the current <see cref="GameVersion"/>
57
        /// object.
58
        /// </summary>
59
        public int Build => _build;
2✔
60

61
        /// <summary>
62
        /// Gets whether or not the major component of the version number for the current <see cref="GameVersion"/>
63
        /// object is defined.
64
        /// </summary>
65
        public bool IsMajorDefined => _major != Undefined;
2✔
66

67
        /// <summary>
68
        /// Gets whether or not the minor component of the version number for the current <see cref="GameVersion"/>
69
        /// object is defined.
70
        /// </summary>
71
        public bool IsMinorDefined => _minor != Undefined;
2✔
72

73
        /// <summary>
74
        /// Gets whether or not the patch component of the version number for the current <see cref="GameVersion"/>
75
        /// object is defined.
76
        /// </summary>
77
        public bool IsPatchDefined => _patch != Undefined;
2✔
78

79
        /// <summary>
80
        /// Gets whether or not the build component of the version number for the current <see cref="GameVersion"/>
81
        /// object is defined.
82
        /// </summary>
83
        public bool IsBuildDefined => _build != Undefined;
2✔
84

85
        /// <summary>
86
        /// Indicates whether or not all components of the current <see cref="GameVersion"/> are defined.
87
        /// </summary>
88
        public bool IsFullyDefined => IsMajorDefined && IsMinorDefined && IsPatchDefined && IsBuildDefined;
2✔
89

90
        /// <summary>
91
        /// Indicates wheter or not all the components of the current <see cref="GameVersion"/> are undefined.
92
        /// </summary>
93
        public bool IsAny => !IsMajorDefined && !IsMinorDefined && !IsPatchDefined && !IsBuildDefined;
2!
94

95
        /// <summary>
96
        /// Provide this resource string to other DLLs outside core
97
        /// /// </summary>
UNCOV
98
        public static string AnyString => Properties.Resources.GameVersionYalovAny;
×
99

100
        /// <summary>
101
        /// Check whether a version is null or Any.
102
        /// We group them here because they mean the same thing.
103
        /// </summary>
104
        /// <param name="v">The version to check</param>
105
        /// <returns>
106
        /// True if null or Any, false otherwise
107
        /// </returns>
108

109
        public static bool IsNullOrAny([NotNullWhen(false)] GameVersion? v) => v == null || v.IsAny;
2✔
110

111
        /// <summary>
112
        /// Initialize a new instance of the <see cref="GameVersion"/> class with all components unspecified.
113
        /// </summary>
114
        public GameVersion()
2✔
115
        {
2✔
116
            _major = Undefined;
2✔
117
            _minor = Undefined;
2✔
118
            _patch = Undefined;
2✔
119
            _build = Undefined;
2✔
120

121
            _string = DeriveString(_major, _minor, _patch, _build);
2✔
122
        }
2✔
123

124
        /// <summary>
125
        /// Initialize a new instance of the <see cref="GameVersion"/> class using the specified major value.
126
        /// </summary>
127
        /// <param name="major">The major version number.</param>
128
        public GameVersion(int major)
2✔
129
        {
2✔
130
            if (major < 0)
2✔
131
            {
2✔
132
                throw new ArgumentOutOfRangeException(nameof(major), major.ToString());
2✔
133
            }
134

135
            _major = major;
2✔
136
            _minor = Undefined;
2✔
137
            _patch = Undefined;
2✔
138
            _build = Undefined;
2✔
139

140
            _string = DeriveString(_major, _minor, _patch, _build);
2✔
141
        }
2✔
142

143
        /// <summary>
144
        /// Initialize a new instance of the <see cref="GameVersion"/> class using the specified major and minor
145
        /// values.
146
        /// </summary>
147
        /// <param name="major">The major version number.</param>
148
        /// <param name="minor">The minor version number.</param>
149
        public GameVersion(int major, int minor)
2✔
150
        {
2✔
151
            if (major < 0)
2✔
152
            {
2✔
153
                throw new ArgumentOutOfRangeException(nameof(major), major.ToString());
2✔
154
            }
155

156
            if (minor < 0)
2✔
157
            {
2✔
158
                throw new ArgumentOutOfRangeException(nameof(minor), minor.ToString());
2✔
159
            }
160

161
            _major = major;
2✔
162
            _minor = minor;
2✔
163
            _patch = Undefined;
2✔
164
            _build = Undefined;
2✔
165

166
            _string = DeriveString(_major, _minor, _patch, _build);
2✔
167
        }
2✔
168

169
        /// <summary>
170
        /// Initialize a new instance of the <see cref="GameVersion"/> class using the specified major, minor, and
171
        /// patch values.
172
        /// </summary>
173
        /// <param name="major">The major version number.</param>
174
        /// <param name="minor">The minor version number.</param>
175
        /// <param name="patch">The patch version number.</param>
176
        public GameVersion(int major, int minor, int patch)
2✔
177
        {
2✔
178
            if (major < 0)
2✔
179
            {
2✔
180
                throw new ArgumentOutOfRangeException(nameof(major), major.ToString());
2✔
181
            }
182

183
            if (minor < 0)
2✔
184
            {
2✔
185
                throw new ArgumentOutOfRangeException(nameof(minor), minor.ToString());
2✔
186
            }
187

188
            if (patch < 0)
2✔
189
            {
2✔
190
                throw new ArgumentOutOfRangeException(nameof(patch), patch.ToString());
2✔
191
            }
192

193
            _major = major;
2✔
194
            _minor = minor;
2✔
195
            _patch = patch;
2✔
196
            _build = Undefined;
2✔
197

198
            _string = DeriveString(_major, _minor, _patch, _build);
2✔
199
        }
2✔
200

201
        /// <summary>
202
        /// Initialize a new instance of the <see cref="GameVersion"/> class using the specified major, minor, patch,
203
        /// and build values.
204
        /// </summary>
205
        /// <param name="major">The major version number.</param>
206
        /// <param name="minor">The minor version number.</param>
207
        /// <param name="patch">The patch version number.</param>
208
        /// <param name="build">The build verison number.</param>
209
        public GameVersion(int major, int minor, int patch, int build)
2✔
210
        {
2✔
211
            if (major < 0)
2✔
212
            {
2✔
213
                throw new ArgumentOutOfRangeException(nameof(major), major, $"{major}");
2✔
214
            }
215

216
            if (minor < 0)
2✔
217
            {
2✔
218
                throw new ArgumentOutOfRangeException(nameof(minor), minor, $"{minor}");
2✔
219
            }
220

221
            if (patch < 0)
2✔
222
            {
2✔
223
                throw new ArgumentOutOfRangeException(nameof(patch), patch, $"{patch}");
2✔
224
            }
225

226
            if (build < 0)
2✔
227
            {
2✔
228
                throw new ArgumentOutOfRangeException(nameof(build), build, $"{build}");
2✔
229
            }
230

231
            _major = major;
2✔
232
            _minor = minor;
2✔
233
            _patch = patch;
2✔
234
            _build = build;
2✔
235

236
            _string = DeriveString(_major, _minor, _patch, _build);
2✔
237
        }
2✔
238

239
        /// <summary>
240
        /// Converts the value of the current <see cref="GameVersion"/> to its equivalent <see cref="string"/>
241
        /// representation.
242
        /// </summary>
243
        /// <returns>
244
        /// <para>
245
        /// The <see cref="string"/> representation of the values of the major, minor, patch, and build components of
246
        /// the current <see cref="GameVersion"/> object as depicted in the following format. Each component is
247
        /// separated by a period character ('.'). Square brackets ('[' and ']') indicate a component that will not
248
        /// appear in the return value if the component is not defined:
249
        /// </para>
250
        /// <para>
251
        /// [<i>major</i>[.<i>minor</i>[.<i>patch</i>[.<i>build</i>]]]]
252
        /// </para>
253
        /// <para>
254
        /// For example, if you create a <see cref="GameVersion"/> object using the constructor <c>GameVersion(1,1)</c>,
255
        /// the returned string is "1.1". If you create a <see cref="GameVersion"/> using the constructor (1,3,4,2),
256
        /// the returned string is "1.3.4.2".
257
        /// </para>
258
        /// <para>
259
        /// If the current <see cref="GameVersion"/> is totally undefined the return value will be <c>null</c>.
260
        /// </para>
261
        /// </returns>
262
        public override string? ToString() => _string;
2✔
263

264
        /// <summary>
265
        /// Strip off the build number if it's defined
266
        /// </summary>
267
        /// <returns>A GameVersion equal to this but without a build number</returns>
268
        public GameVersion WithoutBuild => IsBuildDefined ? new GameVersion(_major, _minor, _patch)
2✔
269
                                                          : this;
270

271
        /// <summary>
272
        /// Converts the value of the current <see cref="GameVersion"/> to its equivalent
273
        /// <see cref="GameVersionRange"/>.
274
        /// </summary>
275
        /// <returns>
276
        /// <para>
277
        /// A <see cref="GameVersionRange"/> which specifies a set of versions equivalent to the current
278
        /// <see cref="GameVersion"/>.
279
        /// </para>
280
        /// <para>
281
        /// For example, the version "1.0.0.0" would be equivalent to the range ["1.0.0.0", "1.0.0.0"], while the
282
        /// version "1.0" would be equivalent to the range ["1.0.0.0", "1.1.0.0"). Where '[' and ']' represent
283
        /// inclusive bounds and '(' and ')' represent exclusive bounds.
284
        /// </para>
285
        /// </returns>
286
        public GameVersionRange ToVersionRange()
287
        {
2✔
288
            GameVersionBound lower;
289
            GameVersionBound upper;
290

291
            if (IsBuildDefined)
2✔
292
            {
2✔
293
                lower = new GameVersionBound(this, inclusive: true);
2✔
294
                upper = new GameVersionBound(this, inclusive: true);
2✔
295
            }
2✔
296
            else if (IsPatchDefined)
2✔
297
            {
2✔
298
                lower = new GameVersionBound(new GameVersion(Major, Minor, Patch, 0), inclusive: true);
2✔
299
                upper = new GameVersionBound(new GameVersion(Major, Minor, Patch + 1, 0), inclusive: false);
2✔
300
            }
2✔
301
            else if (IsMinorDefined)
2✔
302
            {
2✔
303
                lower = new GameVersionBound(new GameVersion(Major, Minor, 0, 0), inclusive: true);
2✔
304
                upper = new GameVersionBound(new GameVersion(Major, Minor + 1, 0, 0), inclusive: false);
2✔
305
            }
2✔
306
            else if (IsMajorDefined)
2✔
307
            {
2✔
308
                lower = new GameVersionBound(new GameVersion(Major, 0, 0, 0), inclusive: true);
2✔
309
                upper = new GameVersionBound(new GameVersion(Major + 1, 0, 0, 0), inclusive: false);
2✔
310
            }
2✔
311
            else
312
            {
2✔
313
                lower = GameVersionBound.Unbounded;
2✔
314
                upper = GameVersionBound.Unbounded;
2✔
315
            }
2✔
316

317
            return new GameVersionRange(lower, upper);
2✔
318
        }
2✔
319

320
        /// <summary>
321
        /// Converts the string representation of a version number to an equivalent <see cref="GameVersion"/> object.
322
        /// </summary>
323
        /// <param name="input">A string that contains a version number to convert.</param>
324
        /// <returns>
325
        /// A <see cref="GameVersion"/> object that is equivalent to the version number specified in the
326
        /// input parameter.
327
        /// </returns>
328
        public static GameVersion Parse(string input)
329
        {
2✔
330
            if (TryParse(input, out GameVersion? result) && result is not null)
2✔
331
            {
2✔
332
                return result;
2✔
333
            }
334
            else
335
            {
2✔
336
                throw new FormatException();
2✔
337
            }
338
        }
2✔
339

340
        /// <summary>
341
        /// Tries to convert the string representation of a version number to an equivalent <see cref="GameVersion"/>
342
        /// object and returns a value that indicates whether the conversion succeeded.
343
        /// </summary>
344
        /// <param name="input">
345
        /// A string that contains a version number to convert.
346
        /// </param>
347
        /// <param name="result">
348
        /// When this method returns <c>true</c>, contains the <see cref="GameVersion"/> equivalent of the number that
349
        /// is contained in input. When this method returns <c>false</c>, the value is unspecified.
350
        /// </param>
351
        /// <returns>
352
        /// <c>true</c> if the input parameter was converted successfully; otherwise, <c>false</c>.
353
        /// </returns>
354
        public static bool TryParse(string? input,
355
                                    [NotNullWhen(returnValue: true)] out GameVersion? result)
356
        {
2✔
357
            result = null;
2✔
358

359
            if (input is null)
2✔
360
            {
2✔
361
                return false;
2✔
362
            }
363

364
            if (input == "any")
2✔
365
            {
2✔
366
                result = Any;
2✔
367
                return true;
2✔
368
            }
369

370
            var major = Undefined;
2✔
371
            var minor = Undefined;
2✔
372
            var patch = Undefined;
2✔
373
            var build = Undefined;
2✔
374

375
            var match = Pattern.Match(input.Trim());
2✔
376

377
            if (match.Success)
2✔
378
            {
2✔
379
                var majorGroup = match.Groups["major"];
2✔
380
                var minorGroup = match.Groups["minor"];
2✔
381
                var patchGroup = match.Groups["patch"];
2✔
382
                var buildGroup = match.Groups["build"];
2✔
383

384
                if (majorGroup.Success)
2!
385
                {
2✔
386
                    if (!int.TryParse(majorGroup.Value, out major))
2✔
387
                    {
2✔
388
                        return false;
2✔
389
                    }
390

391
                    if (major is < 0 or int.MaxValue)
2!
392
                    {
×
393
                        major = Undefined;
×
394
                    }
×
395
                }
2✔
396

397
                if (minorGroup.Success)
2✔
398
                {
2✔
399
                    if (!int.TryParse(minorGroup.Value, out minor))
2✔
400
                    {
2✔
401
                        return false;
2✔
402
                    }
403

404
                    if (minor is < 0 or int.MaxValue)
2!
405
                    {
×
406
                        minor = Undefined;
×
407
                    }
×
408
                }
2✔
409

410
                if (patchGroup.Success)
2✔
411
                {
2✔
412
                    if (!int.TryParse(patchGroup.Value, out patch))
2✔
413
                    {
2✔
414
                        return false;
2✔
415
                    }
416

417
                    if (patch is < 0 or int.MaxValue)
2!
418
                    {
×
419
                        patch = Undefined;
×
420
                    }
×
421
                }
2✔
422

423
                if (buildGroup.Success)
2✔
424
                {
2✔
425
                    if (!int.TryParse(buildGroup.Value, out build))
2✔
426
                    {
2✔
427
                        return false;
2✔
428
                    }
429

430
                    if (build is < 0 or int.MaxValue)
2!
431
                    {
×
432
                        build = Undefined;
×
433
                    }
×
434
                }
2✔
435

436
                if (minor == Undefined)
2✔
437
                {
2✔
438
                    result = new GameVersion(major);
2✔
439
                }
2✔
440
                else if (patch == Undefined)
2✔
441
                {
2✔
442
                    result = new GameVersion(major, minor);
2✔
443
                }
2✔
444
                else if (build == Undefined)
2✔
445
                {
2✔
446
                    result = new GameVersion(major, minor, patch);
2✔
447
                }
2✔
448
                else
449
                {
2✔
450
                    result = new GameVersion(major, minor, patch, build);
2✔
451
                }
2✔
452

453
                return true;
2✔
454
            }
455
            else
456
            {
2✔
457
                return false;
2✔
458
            }
459
        }
2✔
460

461
        /// <summary>
462
        /// Searches the build map if the version is a valid, known KSP version.
463
        /// </summary>
464
        /// <returns><c>true</c>, if version is in the build map, <c>false</c> otherwise.</returns>
465
        public bool InBuildMap(IGame game)
466
        {
2✔
467
            List<GameVersion> knownVersions = game.KnownVersions;
2✔
468

469
            foreach (GameVersion ver in knownVersions)
5✔
470
            {
2✔
471
                if (ver.Major == Major && ver.Minor == Minor && ver.Patch == Patch)
2✔
472
                {
2✔
473
                    // If it found a matching maj, min and patch,
474
                    // test if the build numbers are the same too, but ignore if the
475
                    // version is NOT build defined.
476
                    if (ver.Build == Build || !IsBuildDefined)
2!
477
                    {
2✔
478
                        return true;
2✔
479
                    }
480
                }
×
481
            }
2✔
482
            return false;
2✔
483
        }
2✔
484

485
        /// <summary>
486
        /// Raises a selection dialog for choosing a specific KSP version, if it is not fully defined yet.
487
        /// If a build number is specified but not known, it presents a list of all builds
488
        /// of the patch range.
489
        /// Needs at least a Major and Minor (doesn't make sense else).
490
        /// </summary>
491
        /// <returns>A complete GameVersion object</returns>
492
        /// <param name="game">The game whose versions are to be selected</param>
493
        /// <param name="user">A IUser instance, to raise the corresponding dialog</param>
494
        public GameVersion RaiseVersionSelectionDialog(IGame game, IUser? user)
495
        {
2✔
496
            if (IsFullyDefined && InBuildMap(game))
2!
497
            {
×
498
                // The specified version is complete and known :hooray:. Return this instance.
499
                return this;
×
500
            }
501
            else if (!IsMajorDefined || !IsMinorDefined)
2!
502
            {
×
503
                throw new BadGameVersionKraken(Properties.Resources.GameVersionSelectNeedOne);
×
504
            }
505
            else
506
            {
2✔
507
                // Get all known versions out of the build map.
508
                var knownVersions = game.KnownVersions;
2✔
509
                List<GameVersion> possibleVersions = new List<GameVersion>();
2✔
510

511
                // Default message passed to RaiseSelectionDialog.
512
                string message = Properties.Resources.GameVersionSelectHeader;
2✔
513

514
                // Find the versions which are part of the range.
515
                foreach (GameVersion ver in knownVersions)
5✔
516
                {
2✔
517
                    // If we only have Major and Minor -> compare these two.
518
                    if (!IsPatchDefined)
2!
519
                    {
×
520
                        if (Major == ver.Major && Minor == ver.Minor)
×
521
                        {
×
522
                            possibleVersions.Add(ver);
×
523
                        }
×
524
                    }
×
525
                    // If we also have Patch -> compare it too.
526
                    else if (!IsBuildDefined)
2!
527
                    {
2✔
528
                        if (Major == ver.Major && Minor == ver.Minor && Patch == ver.Patch)
2✔
529
                        {
2✔
530
                            possibleVersions.Add(ver);
2✔
531
                        }
2✔
532
                    }
2✔
533
                    // And if we are here, there's a build number not known in the build map.
534
                    // Only compare Major, Minor, Patch and adjust the message.
535
                    else
536
                    {
×
537
                        message = Properties.Resources.GameVersionSelectBuildHeader;
×
538
                        if (Major == ver.Major && Minor == ver.Minor && Patch == ver.Patch)
×
539
                        {
×
540
                            possibleVersions.Add(ver);
×
541
                        }
×
542
                    }
×
543
                }
2✔
544

545
                // Now do some checks and raise the selection dialog.
546
                if (possibleVersions.Count == 0)
2!
547
                {
×
548
                    // No version found in the map. Happens for future or other unknown versions.
549
                    throw new BadGameVersionKraken(Properties.Resources.GameVersionNotKnown);
×
550
                }
551
                else if (possibleVersions.Count == 1)
2!
552
                {
2✔
553
                    // Lucky, there's only one possible version. Happens f.e. if there's only one build per patch (especially the case for newer versions).
554
                    return possibleVersions.ElementAt(0);
2✔
555
                }
556
                else if (user == null || user.Headless)
×
557
                {
×
558
                    return possibleVersions.Last();
×
559
                }
560
                else
561
                {
×
562
                    int choosen = user.RaiseSelectionDialog(message, possibleVersions.ToArray());
×
563
                    if (choosen >= 0 && choosen < possibleVersions.Count)
×
564
                    {
×
565
                        return possibleVersions.ElementAt(choosen);
×
566
                    }
567
                    else
568
                    {
×
569
                        throw new CancelledActionKraken();
×
570
                    }
571
                }
572
            }
573
        }
2✔
574

575
        private static string? DeriveString(int major, int minor, int patch, int build)
576
        {
2✔
577
            var sb = new StringBuilder();
2✔
578

579
            if (major != Undefined)
2✔
580
            {
2✔
581
                sb.Append(major);
2✔
582
            }
2✔
583

584
            if (minor != Undefined)
2✔
585
            {
2✔
586
                sb.Append(".");
2✔
587
                sb.Append(minor);
2✔
588
            }
2✔
589

590
            if (patch != Undefined)
2✔
591
            {
2✔
592
                sb.Append(".");
2✔
593
                sb.Append(patch);
2✔
594
            }
2✔
595

596
            if (build != Undefined)
2✔
597
            {
2✔
598
                sb.Append(".");
2✔
599
                sb.Append(build);
2✔
600
            }
2✔
601

602
            var s = sb.ToString();
2✔
603

604
            return s.Equals(string.Empty) ? null : s;
2✔
605
        }
2✔
606

607
        /// <summary>
608
        /// Update the game versions of a module.
609
        /// Final range will be the union of the previous and new ranges.
610
        /// Note that this means we always increase, never decrease, compatibility.
611
        /// </summary>
612
        /// <param name="json">The module being inflated</param>
613
        /// <param name="ver">The single game version</param>
614
        /// <param name="minVer">The minimum game version</param>
615
        /// <param name="maxVer">The maximum game version</param>
616
        public static void SetJsonCompatibility(JObject      json,
617
                                                GameVersion? ver,
618
                                                GameVersion? minVer,
619
                                                GameVersion? maxVer)
620
        {
2✔
621
            // Get the minimum and maximum game versions that already exist in the metadata.
622
            // Use specific game version if min/max don't exist.
623
            var existingMinStr = json.Value<string>("ksp_version_min") ?? json.Value<string>("ksp_version");
2✔
624
            var existingMaxStr = json.Value<string>("ksp_version_max") ?? json.Value<string>("ksp_version");
2✔
625

626
            var existingMin = existingMinStr == null ? null : Parse(existingMinStr);
2✔
627
            var existingMax = existingMaxStr == null ? null : Parse(existingMaxStr);
2✔
628

629
            GameVersion? avcMin, avcMax;
630
            if (minVer == null && maxVer == null)
2✔
631
            {
2✔
632
                // Use specific game version if min/max don't exist
633
                avcMin = avcMax = ver;
2✔
634
            }
2✔
635
            else
636
            {
2✔
637
                avcMin = minVer;
2✔
638
                avcMax = maxVer;
2✔
639
            }
2✔
640

641
            // Now calculate the minimum and maximum KSP versions between both the existing metadata and the
642
            // AVC file.
643
            var gameVerMins  = new List<GameVersion?>();
2✔
644
            var gameVerMaxes = new List<GameVersion?>();
2✔
645

646
            if (!IsNullOrAny(existingMin))
2✔
647
            {
2✔
648
                gameVerMins.Add(existingMin);
2✔
649
            }
2✔
650

651
            if (!IsNullOrAny(avcMin))
2✔
652
            {
2✔
653
                gameVerMins.Add(avcMin);
2✔
654
            }
2✔
655

656
            if (!IsNullOrAny(existingMax))
2✔
657
            {
2✔
658
                gameVerMaxes.Add(existingMax);
2✔
659
            }
2✔
660

661
            if (!IsNullOrAny(avcMax))
2✔
662
            {
2✔
663
                gameVerMaxes.Add(avcMax);
2✔
664
            }
2✔
665

666
            var gameVerMin = gameVerMins.DefaultIfEmpty(null).Min();
2✔
667
            var gameVerMax = gameVerMaxes.DefaultIfEmpty(null).Max();
2✔
668

669
            if (gameVerMin != null || gameVerMax != null)
2✔
670
            {
2✔
671
                // If we have either a minimum or maximum game version, remove all existing game version
672
                // information from the metadata.
673
                json.Remove("ksp_version");
2✔
674
                json.Remove("ksp_version_min");
2✔
675
                json.Remove("ksp_version_max");
2✔
676

677
                if (gameVerMin != null && gameVerMax != null)
2✔
678
                {
2✔
679
                    // If we have both a minimum and maximum game version...
680
                    if (gameVerMin.Equals(gameVerMax))
2✔
681
                    {
2✔
682
                        // ...and they are equal, then just set ksp_version
683
                        log.DebugFormat("Min and max game versions are same, setting ksp_version");
2✔
684
                        json["ksp_version"] = gameVerMin.ToString();
2✔
685
                    }
2✔
686
                    else
687
                    {
2✔
688
                        // ...otherwise set both ksp_version_min and ksp_version_max
689
                        log.DebugFormat("Min and max game versions are different, setting both");
2✔
690
                        json["ksp_version_min"] = gameVerMin.ToString();
2✔
691
                        json["ksp_version_max"] = gameVerMax.ToString();
2✔
692
                    }
2✔
693
                }
2✔
694
                else
695
                {
2✔
696
                    // If we have only one or the other then set which ever is applicable
697
                    if (gameVerMin != null)
2✔
698
                    {
2✔
699
                        log.DebugFormat("Only min game version is set");
2✔
700
                        json["ksp_version_min"] = gameVerMin.ToString();
2✔
701
                    }
2✔
702
                    if (gameVerMax != null)
2✔
703
                    {
2✔
704
                        log.DebugFormat("Only max game version is set");
2✔
705
                        json["ksp_version_max"] = gameVerMax.ToString();
2✔
706
                    }
2✔
707
                }
2✔
708
            }
2✔
709
        }
2✔
710

711
        private static readonly ILog log = LogManager.GetLogger(typeof(GameVersion));
2✔
712
    }
713

714
    public sealed partial class GameVersion : IEquatable<GameVersion>
715
    {
716
        /// <summary>
717
        /// Returns a value indicating whether the current <see cref="GameVersion"/> object and specified
718
        /// <see cref="GameVersion"/> object represent the same value.
719
        /// </summary>
720
        /// <param name="obj">
721
        /// A <see cref="GameVersion"/> object to compare to the current <see cref="GameVersion"/> object, or
722
        /// <c>null</c>.
723
        /// </param>
724
        /// <returns>
725
        /// <c>true</c> if every component of the current <see cref="GameVersion"/> matches the corresponding component
726
        /// of the obj parameter; otherwise, <c>false</c>.
727
        /// </returns>
728
        public bool Equals(GameVersion? obj)
729
            => obj is not null
2✔
730
                && (ReferenceEquals(obj, this)
731
                    || (_major == obj._major
732
                        && _minor == obj._minor
733
                        && _patch == obj._patch
734
                        && _build == obj._build));
735

736
        /// <summary>
737
        /// Returns a value indicating whether the current <see cref="GameVersion"/> object is equal to a specified
738
        /// object.
739
        /// </summary>
740
        /// <param name="obj">
741
        /// An object to compare with the current <see cref="GameVersion"/> object, or <c>null</c>.
742
        /// </param>
743
        /// <returns>
744
        /// <c>true</c> if the current <see cref="GameVersion"/> object and obj are both
745
        /// <see cref="GameVersion"/> objects and every component of the current <see cref="GameVersion"/> object
746
        /// matches the corresponding component of obj; otherwise, <c>false</c>.
747
        /// </returns>
748
        public override bool Equals(object? obj)
749
            => obj is not null
2!
750
                && (ReferenceEquals(obj, this)
751
                    || (obj is GameVersion gv && Equals(gv)));
752

753
        /// <summary>
754
        /// Returns a hash code for the current <see cref="GameVersion"/> object.
755
        /// </summary>
756
        /// <returns>A 32-bit signed integer hash code.</returns>
757
        public override int GetHashCode()
758
        #if NET5_0_OR_GREATER
759
            => HashCode.Combine(_major, _minor, _patch, _build);
760
        #else
761
            => (_major, _minor, _patch, _build).GetHashCode();
2✔
762
        #endif
763

764
        /// <summary>
765
        /// Determines whether two specified <see cref="GameVersion"/> objects are equal.
766
        /// </summary>
767
        /// <param name="v1">The first <see cref="GameVersion"/> object.</param>
768
        /// <param name="v2">The second <see cref="GameVersion"/> object.</param>
769
        /// <returns><c>true</c> if v1 equals v2; otherwise, <c>false</c>.</returns>
770
        public static bool operator ==(GameVersion? v1, GameVersion? v2)
771
            => Equals(v1, v2);
2✔
772

773
        /// <summary>
774
        /// Determines whether two specified <see cref="GameVersion"/> objects are not equal.
775
        /// </summary>
776
        /// <param name="v1">The first <see cref="GameVersion"/> object.</param>
777
        /// <param name="v2">The second <see cref="GameVersion"/> object.</param>
778
        /// <returns>
779
        /// <c>true</c> if v1 does not equal v2; otherwise, <c>false</c>.
780
        /// </returns>
781
        public static bool operator !=(GameVersion? v1, GameVersion? v2)
782
            => !Equals(v1, v2);
2✔
783
    }
784

785
    public sealed partial class GameVersion : IComparable, IComparable<GameVersion>
786
    {
787
        /// <summary>
788
        /// Compares the current <see cref="GameVersion"/> object to a specified object and returns an indication of
789
        /// their relative values.
790
        /// </summary>
791
        /// <param name="obj">An object to compare, or <c>null</c>.</param>
792
        /// <returns>
793
        /// A signed integer that indicates the relative values of the two objects, as shown in the following table.
794
        /// <list type="table">
795
        /// <listheader>
796
        /// <term>Return value</term>
797
        /// <description>Meaning</description>
798
        /// </listheader>
799
        /// <item>
800
        /// <term>Less than zero</term>
801
        /// <description>
802
        /// The current <see cref="GameVersion"/> object is a version before obj.
803
        /// </description>
804
        /// </item>
805
        /// <item>
806
        /// <term>Zero</term>
807
        /// <description>
808
        /// The current <see cref="GameVersion"/> object is the same version as obj.
809
        /// </description>
810
        /// </item>
811
        /// <item>
812
        /// <term>Greater than zero</term>
813
        /// <description>
814
        /// <para>
815
        /// The current <see cref="GameVersion"/> object is a version subsequent to obj.
816
        /// </para>
817
        /// </description>
818
        /// </item>
819
        /// </list>
820
        /// </returns>
821
        public int CompareTo(object? obj)
822
        {
2✔
823
            if (obj is null)
2✔
824
            {
2✔
825
                throw new ArgumentNullException(nameof(obj));
2✔
826
            }
827

828
            var objGameVersion = obj as GameVersion;
2✔
829

830
            if (objGameVersion != null)
2✔
831
            {
2✔
832
                return CompareTo(objGameVersion);
2✔
833
            }
834
            else
835
            {
2✔
836
                throw new ArgumentException("Object must be of type GameVersion.");
2✔
837
            }
838
        }
2✔
839

840
        /// <summary>
841
        /// Compares the current <see cref="GameVersion"/> object to a specified object and returns an indication of
842
        /// their relative values.
843
        /// </summary>
844
        /// <param name="other">An object to compare.</param>
845
        /// <returns>
846
        /// A signed integer that indicates the relative values of the two objects, as shown in the following table.
847
        /// <list type="table">
848
        /// <listheader>
849
        /// <term>Return value</term>
850
        /// <description>Meaning</description>
851
        /// </listheader>
852
        /// <item>
853
        /// <term>Less than zero</term>
854
        /// <description>
855
        /// The current <see cref="GameVersion"/> object is a version before other.
856
        /// </description>
857
        /// </item>
858
        /// <item>
859
        /// <term>Zero</term>
860
        /// <description>
861
        /// The current <see cref="GameVersion"/> object is the same version as other.
862
        /// </description>
863
        /// </item>
864
        /// <item>
865
        /// <term>Greater than zero</term>
866
        /// <description>
867
        /// <para>
868
        /// The current <see cref="GameVersion"/> object is a version subsequent to other.
869
        /// </para>
870
        /// </description>
871
        /// </item>
872
        /// </list>
873
        /// </returns>
874
        public int CompareTo(GameVersion? other)
875
        {
2✔
876
            if (other is null)
2✔
877
            {
2✔
878
                throw new ArgumentNullException(nameof(other));
2✔
879
            }
880

881
            if (Equals(this, other))
2✔
882
            {
2✔
883
                return 0;
2✔
884
            }
885

886
            var majorCompare = _major.CompareTo(other._major);
2✔
887

888
            if (majorCompare == 0)
2✔
889
            {
2✔
890
                var minorCompare = _minor.CompareTo(other._minor);
2✔
891

892
                if (minorCompare == 0)
2✔
893
                {
2✔
894
                    var patchCompare = _patch.CompareTo(other._patch);
2✔
895

896
                    return patchCompare == 0 ? _build.CompareTo(other._build) : patchCompare;
2✔
897
                }
898
                else
899
                {
2✔
900
                    return minorCompare;
2✔
901
                }
902
            }
903
            else
904
            {
2✔
905
                return majorCompare;
2✔
906
            }
907
        }
2✔
908

909
        /// <summary>
910
        /// Determines whether the first specified <see cref="GameVersion"/> object is less than the second specified
911
        /// <see cref="GameVersion"/> object.
912
        /// </summary>
913
        /// <param name="left">The first <see cref="GameVersion"/> object.</param>
914
        /// <param name="right">The second <see cref="GameVersion"/> object.</param>
915
        /// <returns>
916
        /// <c>true</c> if left is less than right; otherwise, <c>false</c>.
917
        /// </returns>
918
        public static bool operator <(GameVersion left, GameVersion right)
919
            => left.CompareTo(right) < 0;
2✔
920

921
        /// <summary>
922
        /// Determines whether the first specified <see cref="GameVersion"/> object is greater than the second
923
        /// specified <see cref="ModuleVersion"/> object.
924
        /// </summary>
925
        /// <param name="left">The first <see cref="GameVersion"/> object.</param>
926
        /// <param name="right">The second <see cref="GameVersion"/> object.</param>
927
        /// <returns>
928
        /// <c>true</c> if left is greater than right; otherwise, <c>false</c>.
929
        /// </returns>
930
        public static bool operator >(GameVersion left, GameVersion right)
931
            => left.CompareTo(right) > 0;
2✔
932

933
        /// <summary>
934
        /// Determines whether the first specified <see cref="GameVersion"/> object is less than or equal to the second
935
        /// specified <see cref="GameVersion"/> object.
936
        /// </summary>
937
        /// <param name="left">The first <see cref="GameVersion"/> object.</param>
938
        /// <param name="right">The second <see cref="GameVersion"/> object.</param>
939
        /// <returns>
940
        /// <c>true</c> if left is less than or equal to right; otherwise, <c>false</c>.
941
        /// </returns>
942
        public static bool operator <=(GameVersion left, GameVersion right)
943
            => left.CompareTo(right) <= 0;
2✔
944

945
        /// <summary>
946
        /// Determines whether the first specified <see cref="GameVersion"/> object is greater than or equal to the
947
        /// second specified <see cref="GameVersion"/> object.
948
        /// </summary>
949
        /// <param name="left">The first <see cref="GameVersion"/> object.</param>
950
        /// <param name="right">The second <see cref="GameVersion"/> object.</param>
951
        /// <returns>
952
        /// <c>true</c> if left is greater than or equal to right; otherwise, <c>false</c>.
953
        /// </returns>
954
        public static bool operator >=(GameVersion left, GameVersion right)
955
            => left.CompareTo(right) >= 0;
2✔
956
    }
957

958
    public sealed class GameVersionJsonConverter : JsonConverter
959
    {
960
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
961
        {
2✔
962
            writer.WriteValue(value?.ToString());
2✔
963
        }
2✔
964

965
        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
966
        {
2✔
967
            var value = reader.Value?.ToString();
2✔
968

969
            switch (value)
2✔
970
            {
971
                case null:
972
                    return null;
2✔
973

974
                default:
975
                    // For a little while, AVC files which didn't specify a full three-part
976
                    // version number could result in versions like `1.1.`, which cause our
977
                    // code to fail. Here we strip any trailing dot from the version number,
978
                    // which makes them valid again before parsing. CKAN#1780
979

980
                    value = Regex.Replace(value, @"\.$", "");
2✔
981

982
                    if (GameVersion.TryParse(value, out GameVersion? result))
2✔
983
                    {
2✔
984
                        return result;
2✔
985
                    }
986
                    else
987
                    {
2✔
988
                        throw new JsonException(string.Format("Could not parse game version: {0}", value));
2✔
989
                    }
990
            }
991
        }
2✔
992

993
        public override bool CanConvert(Type objectType)
994
            => objectType == typeof(GameVersion);
2✔
995
    }
996
}
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