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

KSP-CKAN / CKAN / 16536011796

26 Jul 2025 04:19AM UTC coverage: 56.351% (+8.5%) from 47.804%
16536011796

Pull #4408

github

web-flow
Merge b51164c06 into 99ebf468c
Pull Request #4408: Add tests for CmdLine

4558 of 8422 branches covered (54.12%)

Branch coverage included in aggregate %.

148 of 273 new or added lines in 28 files covered. (54.21%)

18 existing lines in 5 files now uncovered.

9719 of 16914 relevant lines covered (57.46%)

1.18 hits per line

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

53.35
/Core/GameInstanceManager.cs
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Linq;
5
using System.Transactions;
6
using System.Diagnostics.CodeAnalysis;
7

8
using Autofac;
9
using ChinhDo.Transactions.FileManager;
10
using log4net;
11

12
using CKAN.IO;
13
using CKAN.Versioning;
14
using CKAN.Configuration;
15
using CKAN.Games;
16
using CKAN.Games.KerbalSpaceProgram;
17
using CKAN.Games.KerbalSpaceProgram.GameVersionProviders;
18

19
namespace CKAN
20
{
21
    /// <summary>
22
    /// Manage multiple KSP installs.
23
    /// </summary>
24
    public class GameInstanceManager : IDisposable
25
    {
26
        /// <summary>
27
        /// An IUser object for user interaction.
28
        /// It is initialized during the startup with a ConsoleUser,
29
        /// do not use in functions that could be called by the GUI.
30
        /// </summary>
31
        public IUser          User            { get; set; }
32
        public IConfiguration Configuration   { get; set; }
33
        public GameInstance?  CurrentInstance { get; private set; }
34
        public event Action<GameInstance?, GameInstance?>? InstanceChanged;
35

36
        public NetModuleCache? Cache { get; private set; }
37
        public event Action<NetModuleCache>? CacheChanged;
38

39
        public  SteamLibrary  SteamLibrary => steamLib ??= new SteamLibrary();
×
40
        private SteamLibrary? steamLib;
41

42
        private static readonly ILog log = LogManager.GetLogger(typeof (GameInstanceManager));
2✔
43

44
        private readonly SortedList<string, GameInstance> instances = new SortedList<string, GameInstance>();
2✔
45

46
        public string[] AllInstanceAnchorFiles => KnownGames.knownGames
×
47
            .SelectMany(g => g.InstanceAnchorFiles)
×
48
            .Distinct()
49
            .ToArray();
50

51
        public string? AutoStartInstance
52
        {
53
            get => Configuration.AutoStartInstance != null && HasInstance(Configuration.AutoStartInstance)
2!
54
                       ? Configuration.AutoStartInstance
55
                       : null;
56

57
            private set
58
            {
2✔
59
                if (value != null && !string.IsNullOrEmpty(value) && !HasInstance(value))
2!
60
                {
×
61
                    throw new InvalidKSPInstanceKraken(value);
×
62
                }
63
                Configuration.AutoStartInstance = value;
2✔
64
            }
2✔
65
        }
66

67
        public SortedList<string, GameInstance> Instances => new SortedList<string, GameInstance>(instances);
2✔
68

69
        public GameInstanceManager(IUser user, IConfiguration configuration)
2✔
70
        {
2✔
71
            User = user;
2✔
72
            Configuration = configuration;
2✔
73
            LoadInstances();
2✔
74
            LoadCacheSettings();
2✔
75
        }
2✔
76

77
        /// <summary>
78
        /// Returns the preferred game instance, or null if none can be found.
79
        ///
80
        /// This works by checking to see if we're in a KSP dir first, then the
81
        /// config for an autostart instance, then will try to auto-populate
82
        /// by scanning for the game.
83
        ///
84
        /// This *will not* touch the config if we find a portable install.
85
        ///
86
        /// This *will* run KSP instance autodetection if the config is empty.
87
        ///
88
        /// This *will* set the current instance, or throw an exception if it's already set.
89
        ///
90
        /// Returns null if we have multiple instances, but none of them are preferred.
91
        /// </summary>
92
        public GameInstance? GetPreferredInstance()
93
        {
2✔
94
            CurrentInstance = _GetPreferredInstance();
2✔
95
            return CurrentInstance;
2✔
96
        }
2✔
97

98
        // Actual worker for GetPreferredInstance()
99
        internal GameInstance? _GetPreferredInstance()
100
        {
2✔
101
            foreach (IGame game in KnownGames.knownGames)
5✔
102
            {
2✔
103
                // TODO: Check which ones match, prompt user if >1
104

105
                // First check if we're part of a portable install
106
                // Note that this *does not* register in the config.
107
                string? path = GameInstance.PortableDir(game);
2✔
108

109
                if (path != null)
2!
110
                {
×
111
                    GameInstance portableInst = new GameInstance(
×
112
                        game, path, Properties.Resources.GameInstanceManagerPortable, User);
113
                    if (portableInst.Valid)
×
114
                    {
×
115
                        return portableInst;
×
116
                    }
117
                }
×
118
            }
2✔
119

120
            // If we only know of a single instance, return that.
121
            if (instances.Count == 1 && instances.First().Value.Valid)
2✔
122
            {
2✔
123
                return instances.First().Value;
2✔
124
            }
125

126
            // Return the autostart, if we can find it.
127
            // We check both null and "" as we can't write NULL to the config, so we write an empty string instead
128
            // This is necessary so we can indicate that the user wants to reset the current AutoStartInstance without clearing the config!
129
            if (AutoStartInstance != null
2!
130
                && !string.IsNullOrEmpty(AutoStartInstance)
131
                && instances[AutoStartInstance].Valid)
132
            {
×
133
                return instances[AutoStartInstance];
×
134
            }
135

136
            // If we know of no instances, try to find one.
137
            // Otherwise, we know of too many instances!
138
            // We don't know which one to pick, so we return null.
139
            return instances.Count == 0 ? FindAndRegisterDefaultInstances() : null;
2!
140
        }
2✔
141

142
        /// <summary>
143
        /// Find and register default instances by running
144
        /// game autodetection code. Registers one per known game,
145
        /// uses first found as default.
146
        ///
147
        /// Returns the resulting game instance if found.
148
        /// </summary>
149
        public GameInstance? FindAndRegisterDefaultInstances()
150
        {
×
151
            if (instances.Count != 0)
×
152
            {
×
153
                throw new KSPManagerKraken("Attempted to scan for defaults with instances");
×
154
            }
155
            var found = FindDefaultInstances();
×
156
            foreach (var inst in found)
×
157
            {
×
158
                log.DebugFormat("Registering {0} at {1}...",
×
159
                                inst.Name, inst.GameDir());
160
                AddInstance(inst);
×
161
            }
×
162
            return found.FirstOrDefault();
×
163
        }
×
164

165
        public GameInstance[] FindDefaultInstances()
166
        {
×
167
            var found = KnownGames.knownGames.SelectMany(g =>
×
168
                            SteamLibrary.Games
×
169
                                        .Select(sg => sg.Name is string name && sg.GameDir is DirectoryInfo dir
×
170
                                                          ? new Tuple<string, DirectoryInfo>(name, dir)
171
                                                          : null)
172
                                        .Append(g.MacPath() is DirectoryInfo dir
173
                                                    ? new Tuple<string, DirectoryInfo>(
174
                                                        string.Format(Properties.Resources.GameInstanceManagerAuto,
175
                                                                      g.ShortName),
176
                                                        dir)
177
                                                    : null)
178
                                        .OfType<Tuple<string, DirectoryInfo>>()
179
                                        .Select(tuple => tuple.Item1 != null && g.GameInFolder(tuple.Item2)
×
180
                                                       ? new GameInstance(g, tuple.Item2.FullName,
181
                                                                          tuple.Item1 ?? g.ShortName, User)
182
                                                       : null)
183
                                        .OfType<GameInstance>())
184
                                  .Where(inst => inst.Valid)
×
185
                                  .ToArray();
186
            foreach (var group in found.GroupBy(inst => inst.Name))
×
187
            {
×
188
                if (group.Count() > 1)
×
189
                {
×
190
                    // Make sure the names are unique
191
                    int index = 0;
×
192
                    foreach (var inst in group)
×
193
                    {
×
194
                        // Find an unused name
195
                        string name;
196
                        do
197
                        {
×
198
                            ++index;
×
199
                            name = $"{group.Key} ({++index})";
×
200
                        }
×
201
                        while (found.Any(other => other.Name == name));
×
202
                        inst.Name = name;
×
203
                    }
×
204
                }
×
205
            }
×
206
            return found;
×
207
        }
×
208

209
        /// <summary>
210
        /// Adds a game instance to config.
211
        /// </summary>
212
        /// <returns>The resulting GameInstance object</returns>
213
        /// <exception cref="NotKSPDirKraken">Thrown if the instance is not a valid game instance.</exception>
214
        public GameInstance AddInstance(GameInstance instance)
215
        {
2✔
216
            if (instance.Valid)
2!
217
            {
2✔
218
                string name = instance.Name;
2✔
219
                instances.Add(name, instance);
2✔
220
                Configuration.SetRegistryToInstances(instances);
2✔
221
            }
2✔
222
            else
223
            {
×
224
                throw new NotKSPDirKraken(instance.GameDir());
×
225
            }
226
            return instance;
2✔
227
        }
2✔
228

229
        /// <summary>
230
        /// Adds a game instance to config.
231
        /// </summary>
232
        /// <param name="path">The path of the instance</param>
233
        /// <param name="name">The name of the instance</param>
234
        /// <param name="user">IUser object for interaction</param>
235
        /// <returns>The resulting GameInstance object</returns>
236
        /// <exception cref="NotKSPDirKraken">Thrown if the instance is not a valid game instance.</exception>
237
        public GameInstance? AddInstance(string path, string name, IUser user)
238
        {
2✔
239
            var game = DetermineGame(new DirectoryInfo(path), user);
2✔
240
            return game == null ? null : AddInstance(new GameInstance(game, path, name, user));
2!
241
        }
2✔
242

243
        /// <summary>
244
        /// Clones an existing KSP installation.
245
        /// </summary>
246
        /// <param name="existingInstance">The KSP instance to clone.</param>
247
        /// <param name="newName">The name for the new instance.</param>
248
        /// <param name="newPath">The path where the new instance should be located.</param>
249
        /// <param name="shareStockFolders">True to make junctions or symlinks to stock folders instead of copying</param>
250
        /// <exception cref="InstanceNameTakenKraken">Thrown if the instance name is already in use.</exception>
251
        /// <exception cref="NotKSPDirKraken">Thrown by AddInstance() if created instance is not valid, e.g. if something went wrong with copying.</exception>
252
        /// <exception cref="DirectoryNotFoundKraken">Thrown by CopyDirectory() if directory doesn't exist. Should never be thrown here.</exception>
253
        /// <exception cref="PathErrorKraken">Thrown by CopyDirectory() if the target folder already exists and is not empty.</exception>
254
        /// <exception cref="IOException">Thrown by CopyDirectory() if something goes wrong during the process.</exception>
255
        public void CloneInstance(GameInstance existingInstance,
256
                                  string       newName,
257
                                  string       newPath,
258
                                  bool         shareStockFolders = false)
259
        {
2✔
260
            CloneInstance(existingInstance, newName, newPath,
2✔
261
                          existingInstance.game.LeaveEmptyInClones,
262
                          shareStockFolders);
263
        }
2✔
264

265
        /// <summary>
266
        /// Clones an existing KSP installation.
267
        /// </summary>
268
        /// <param name="existingInstance">The KSP instance to clone.</param>
269
        /// <param name="newName">The name for the new instance.</param>
270
        /// <param name="newPath">The path where the new instance should be located.</param>
271
        /// <param name="leaveEmpty">Dirs whose contents should not be copied</param>
272
        /// <param name="shareStockFolders">True to make junctions or symlinks to stock folders instead of copying</param>
273
        /// <exception cref="InstanceNameTakenKraken">Thrown if the instance name is already in use.</exception>
274
        /// <exception cref="NotKSPDirKraken">Thrown by AddInstance() if created instance is not valid, e.g. if something went wrong with copying.</exception>
275
        /// <exception cref="DirectoryNotFoundKraken">Thrown by CopyDirectory() if directory doesn't exist. Should never be thrown here.</exception>
276
        /// <exception cref="PathErrorKraken">Thrown by CopyDirectory() if the target folder already exists and is not empty.</exception>
277
        /// <exception cref="IOException">Thrown by CopyDirectory() if something goes wrong during the process.</exception>
278
        public void CloneInstance(GameInstance existingInstance,
279
                                  string       newName,
280
                                  string       newPath,
281
                                  string[]     leaveEmpty,
282
                                  bool         shareStockFolders = false)
283
        {
2✔
284
            if (HasInstance(newName))
2!
285
            {
×
286
                throw new InstanceNameTakenKraken(newName);
×
287
            }
288
            if (!existingInstance.Valid)
2✔
289
            {
2✔
290
                throw new NotKSPDirKraken(existingInstance.GameDir(), string.Format(
2✔
291
                    Properties.Resources.GameInstanceCloneInvalid, existingInstance.game.ShortName));
292
            }
293

294
            log.Debug("Copying directory.");
2✔
295
            Utilities.CopyDirectory(existingInstance.GameDir(), newPath,
2✔
296
                                    shareStockFolders ? existingInstance.game.StockFolders
297
                                                      : Array.Empty<string>(),
298
                                    leaveEmpty);
299

300
            // Add the new instance to the config
301
            AddInstance(new GameInstance(existingInstance.game, newPath, newName, User));
2✔
302
        }
2✔
303

304
        /// <summary>
305
        /// Create a new fake KSP instance
306
        /// </summary>
307
        /// <param name="game">The game of the new instance.</param>
308
        /// <param name="newName">The name for the new instance.</param>
309
        /// <param name="newPath">The location of the new instance.</param>
310
        /// <param name="version">The version of the new instance. Should have a build number.</param>
311
        /// <param name="dlcs">The IDlcDetector implementations for the DLCs that should be faked and the requested dlc version as a dictionary.</param>
312
        /// <exception cref="InstanceNameTakenKraken">Thrown if the instance name is already in use.</exception>
313
        /// <exception cref="NotKSPDirKraken">Thrown by AddInstance() if created instance is not valid, e.g. if a write operation didn't complete for whatever reason.</exception>
314
        public void FakeInstance(IGame game, string newName, string newPath, GameVersion version,
315
                                 Dictionary<DLC.IDlcDetector, GameVersion>? dlcs = null)
316
        {
2✔
317
            TxFileManager fileMgr = new TxFileManager();
2✔
318
            using (TransactionScope transaction = CkanTransaction.CreateTransactionScope())
2✔
319
            {
2✔
320
                if (HasInstance(newName))
2!
321
                {
×
322
                    throw new InstanceNameTakenKraken(newName);
×
323
                }
324

325
                if (!version.InBuildMap(game))
2✔
326
                {
2✔
327
                    throw new BadGameVersionKraken(string.Format(
2✔
328
                        Properties.Resources.GameInstanceFakeBadVersion, game.ShortName, version));
329
                }
330
                if (Directory.Exists(newPath) && (Directory.GetFiles(newPath).Length != 0 || Directory.GetDirectories(newPath).Length != 0))
2✔
331
                {
2✔
332
                    throw new BadInstallLocationKraken(Properties.Resources.GameInstanceFakeNotEmpty);
2✔
333
                }
334

335
                log.DebugFormat("Creating folder structure and text files at {0} for {1} version {2}", Path.GetFullPath(newPath), game.ShortName, version.ToString());
2✔
336

337
                // Create a KSP root directory, containing a GameData folder, a buildID.txt/buildID64.txt and a readme.txt
338
                fileMgr.CreateDirectory(newPath);
2✔
339
                fileMgr.CreateDirectory(Path.Combine(newPath, game.PrimaryModDirectoryRelative));
2✔
340
                game.RebuildSubdirectories(newPath);
2✔
341

342
                foreach (var anchor in game.InstanceAnchorFiles)
5✔
343
                {
2✔
344
                    fileMgr.WriteAllText(Path.Combine(newPath, anchor),
2✔
345
                                         version.WithoutBuild.ToString());
346
                }
2✔
347

348
                // Don't write the buildID.txts if we have no build, otherwise it would be -1.
349
                if (version.IsBuildDefined && game is KerbalSpaceProgram)
2!
350
                {
×
351
                    foreach (var b in KspBuildIdVersionProvider.buildIDfilenames)
×
352
                    {
×
353
                        fileMgr.WriteAllText(Path.Combine(newPath, b),
×
354
                                             string.Format("build id = {0}", version.Build));
355
                    }
×
356
                }
×
357

358
                // Create the readme.txt WITHOUT build number
359
                fileMgr.WriteAllText(Path.Combine(newPath, "readme.txt"),
2✔
360
                                     string.Format("Version {0}",
361
                                                   version.WithoutBuild.ToString()));
362

363
                // Create the needed folder structure and the readme.txt for DLCs that should be simulated.
364
                if (dlcs != null)
2✔
365
                {
2✔
366
                    foreach (KeyValuePair<DLC.IDlcDetector, GameVersion> dlc in dlcs)
5✔
367
                    {
2✔
368
                        DLC.IDlcDetector dlcDetector = dlc.Key;
2✔
369
                        GameVersion dlcVersion = dlc.Value;
2✔
370

371
                        if (!dlcDetector.AllowedOnBaseVersion(version))
2✔
372
                        {
2✔
373
                            throw new WrongGameVersionKraken(
2✔
374
                                version,
375
                                string.Format(Properties.Resources.GameInstanceFakeDLCNotAllowed,
376
                                    game.ShortName,
377
                                    dlcDetector.ReleaseGameVersion,
378
                                    dlcDetector.IdentifierBaseName));
379
                        }
380

381
                        string dlcDir = Path.Combine(newPath, dlcDetector.InstallPath());
2✔
382
                        fileMgr.CreateDirectory(dlcDir);
2✔
383
                        fileMgr.WriteAllText(
2✔
384
                            Path.Combine(dlcDir, "readme.txt"),
385
                            string.Format("Version {0}", dlcVersion));
386
                    }
2✔
387
                }
2✔
388

389
                // Add the new instance to the config
390
                GameInstance new_instance = new GameInstance(game, newPath, newName, User);
2✔
391
                AddInstance(new_instance);
2✔
392
                transaction.Complete();
2✔
393
            }
2✔
394
        }
2✔
395

396
        /// <summary>
397
        /// Given a string returns a unused valid instance name by postfixing the string
398
        /// </summary>
399
        /// <returns> A unused valid instance name.</returns>
400
        /// <param name="name">The name to use as a base.</param>
401
        /// <exception cref="Kraken">Could not find a valid name.</exception>
402
        public string GetNextValidInstanceName(string name)
403
        {
2✔
404
            // Check if the current name is valid
405
            if (InstanceNameIsValid(name))
2!
406
            {
×
407
                return name;
×
408
            }
409

410
            // Try appending a number to the name
411
            var validName = Enumerable.Repeat(name, 1000)
2✔
412
                .Select((s, i) => s + " (" + i + ")")
2✔
413
                .FirstOrDefault(InstanceNameIsValid);
414
            if (validName != null)
2!
415
            {
2✔
416
                return validName;
2✔
417
            }
418

419
            // Check if a name with the current timestamp is valid
420
            validName = name + " (" + DateTime.Now + ")";
×
421

422
            if (InstanceNameIsValid(validName))
×
423
            {
×
424
                return validName;
×
425
            }
426

427
            // Give up
428
            throw new Kraken(Properties.Resources.GameInstanceNoValidName);
×
429
        }
2✔
430

431
        /// <summary>
432
        /// Check if the instance name is valid.
433
        /// </summary>
434
        /// <returns><c>true</c>, if name is valid, <c>false</c> otherwise.</returns>
435
        /// <param name="name">Name to check.</param>
436
        private bool InstanceNameIsValid(string name)
437
        {
2✔
438
            // Discard null, empty strings and white space only strings.
439
            // Look for the current name in the list of loaded instances.
440
            return !string.IsNullOrWhiteSpace(name) && !HasInstance(name);
2!
441
        }
2✔
442

443
        /// <summary>
444
        /// Removes the instance from the config and saves.
445
        /// </summary>
446
        public void RemoveInstance(string name)
447
        {
2✔
448
            instances.Remove(name);
2✔
449
            Configuration.SetRegistryToInstances(instances);
2✔
450
        }
2✔
451

452
        /// <summary>
453
        /// Renames an instance in the config and saves.
454
        /// </summary>
455
        public void RenameInstance(string from, string to)
456
        {
2✔
457
            // TODO: What should we do if our target name already exists?
458
            GameInstance ksp = instances[from];
2✔
459
            instances.Remove(from);
2✔
460
            ksp.Name = to;
2✔
461
            instances.Add(to, ksp);
2✔
462
            Configuration.SetRegistryToInstances(instances);
2✔
463
        }
2✔
464

465
        /// <summary>
466
        /// Sets the current instance.
467
        /// Throws an InvalidKSPInstanceKraken if not found.
468
        /// </summary>
469
        public void SetCurrentInstance(string name)
470
        {
2✔
471
            if (!instances.TryGetValue(name, out GameInstance? inst))
2✔
472
            {
2✔
473
                throw new InvalidKSPInstanceKraken(name);
2✔
474
            }
475
            else if (!inst.Valid)
2!
476
            {
×
477
                throw new NotKSPDirKraken(inst.GameDir());
×
478
            }
479
            else
480
            {
2✔
481
                SetCurrentInstance(inst);
2✔
482
            }
2✔
483
        }
2✔
484

485
        public void SetCurrentInstance(GameInstance? instance)
486
        {
2✔
487
            var prev = CurrentInstance;
2✔
488
            // Don't try to Dispose a null CurrentInstance.
489
            if (prev != null && !prev.Equals(instance))
2✔
490
            {
2✔
491
                // Dispose of the old registry manager to release the registry
492
                // (without accidentally locking/loading/etc it).
493
                RegistryManager.DisposeInstance(prev);
2✔
494
            }
2✔
495
            CurrentInstance = instance;
2✔
496
            InstanceChanged?.Invoke(prev, instance);
2!
497
        }
2✔
498

499
        public void SetCurrentInstanceByPath(string path)
500
        {
×
501
            var matchingGames = KnownGames.knownGames
×
502
                .Where(g => g.GameInFolder(new DirectoryInfo(path)))
×
503
                .ToList();
504
            switch (matchingGames.Count)
×
505
            {
506
                case 0:
507
                    throw new NotKSPDirKraken(path);
×
508

509
                case 1:
510
                    GameInstance ksp = new GameInstance(
×
511
                        matchingGames.First(), path, Properties.Resources.GameInstanceByPathName, User);
512
                    if (ksp.Valid)
×
513
                    {
×
514
                        SetCurrentInstance(ksp);
×
515
                    }
×
516
                    else
517
                    {
×
518
                        throw new NotKSPDirKraken(ksp.GameDir());
×
519
                    }
520
                    break;
×
521

522
                default:
523
                    // TODO: Prompt user to choose
524
                    break;
×
525
            }
526
        }
×
527

528
        public GameInstance? InstanceAt(string path)
529
        {
×
530
            var matchingGames = KnownGames.knownGames
×
531
                .Where(g => g.GameInFolder(new DirectoryInfo(path)))
×
532
                .ToList();
533
            switch (matchingGames.Count)
×
534
            {
535
                case 0:
536
                    return null;
×
537

538
                case 1:
539
                    return new GameInstance(
×
540
                        matchingGames.First(), path, Properties.Resources.GameInstanceByPathName, User);
541

542
                default:
543
                    // TODO: Prompt user to choose
544
                    return null;
×
545

546
            }
547
        }
×
548

549
        /// <summary>
550
        /// Sets the autostart instance in the config and saves it.
551
        /// </summary>
552
        public void SetAutoStart(string name)
553
        {
2✔
554
            if (!HasInstance(name))
2✔
555
            {
2✔
556
                throw new InvalidKSPInstanceKraken(name);
2✔
557
            }
558
            else if (!instances[name].Valid)
2!
559
            {
×
560
                throw new NotKSPDirKraken(instances[name].GameDir());
×
561
            }
562
            AutoStartInstance = name;
2✔
563
        }
2✔
564

565
        public bool HasInstance(string name)
566
            => instances.ContainsKey(name);
2✔
567

568
        public void ClearAutoStart()
569
        {
2✔
570
            Configuration.AutoStartInstance = null;
2✔
571
        }
2✔
572

573
        private void LoadInstances()
574
        {
2✔
575
            log.Info("Loading game instances");
2✔
576

577
            instances.Clear();
2✔
578

579
            foreach (Tuple<string, string, string> instance in Configuration.GetInstances())
5✔
580
            {
2✔
581
                var name = instance.Item1;
2✔
582
                var path = instance.Item2;
2✔
583
                var gameName = instance.Item3;
2✔
584
                try
585
                {
2✔
586
                    var game = KnownGames.knownGames.FirstOrDefault(g => g.ShortName == gameName)
2✔
587
                        ?? KnownGames.knownGames.First();
588
                    log.DebugFormat("Loading {0} from {1}", name, path);
2✔
589
                    // Add unconditionally, sort out invalid instances downstream
590
                    instances.Add(name, new GameInstance(game, path, name, User));
2✔
591
                }
2✔
592
                catch (Exception exc)
×
593
                {
×
594
                    // Skip malformed instances (e.g. empty path)
595
                    log.Error($"Failed to load game instance with name=\"{name}\" path=\"{path}\" game=\"{gameName}\"",
×
596
                        exc);
597
                }
×
598
            }
2✔
599
        }
2✔
600

601
        private void LoadCacheSettings()
602
        {
2✔
603
            if (Configuration.DownloadCacheDir != null
2!
604
                && !Directory.Exists(Configuration.DownloadCacheDir))
UNCOV
605
            {
×
606
                try
607
                {
×
608
                    Directory.CreateDirectory(Configuration.DownloadCacheDir);
×
609
                }
×
610
                catch
×
611
                {
×
612
                    // Can't create the configured directory, try reverting it to the default
613
                    Configuration.DownloadCacheDir = null;
×
NEW
614
                    Directory.CreateDirectory(DefaultDownloadCacheDir);
×
615
                }
×
616
            }
×
617

618
            var progress = new ProgressImmediate<int>(p => {});
1✔
619
            if (!TrySetupCache(Configuration.DownloadCacheDir,
2!
620
                               progress,
621
                               out string? failReason)
622
                && Configuration.DownloadCacheDir is { Length: > 0 })
623
            {
×
NEW
624
                log.ErrorFormat("Cache not found at configured path {0}: {1}",
×
625
                                Configuration.DownloadCacheDir, failReason ?? "");
626
                // Fall back to default path to minimize chance of ending up in an invalid state at startup
NEW
627
                TrySetupCache(null, progress, out _);
×
628
            }
×
629
        }
2✔
630

631
        /// <summary>
632
        /// Switch to using a download cache in a new location
633
        /// </summary>
634
        /// <param name="path">Location of folder for new cache</param>
635
        /// <param name="progress">Progress object to report progress to</param>
636
        /// <param name="failureReason">Contains a human readable failure message if the setup failed</param>
637
        /// <returns>
638
        /// true if successful, false otherwise
639
        /// </returns>
640
        public bool TrySetupCache(string?        path,
641
                                  IProgress<int> progress,
642
                                  [NotNullWhen(returnValue: false)]
643
                                  out string?    failureReason)
644
        {
2✔
645
            var origPath  = Configuration.DownloadCacheDir;
2✔
646
            var origCache = Cache;
2✔
647
            try
648
            {
2✔
649
                if (path is { Length: > 0 })
2✔
650
                {
2✔
651
                    Cache = new NetModuleCache(this, path);
2✔
652
                    if (path != origPath)
2✔
653
                    {
2✔
654
                        Configuration.DownloadCacheDir = path;
2✔
655
                    }
2✔
656
                }
2✔
657
                else
658
                {
2✔
659
                    Directory.CreateDirectory(DefaultDownloadCacheDir);
2✔
660
                    Cache = new NetModuleCache(this, DefaultDownloadCacheDir);
2✔
661
                    Configuration.DownloadCacheDir = null;
2✔
662
                }
2✔
663
                if (origPath != null && origCache != null && path != origPath)
2✔
664
                {
2✔
665
                    origCache.GetSizeInfo(out _, out long oldNumBytes, out _);
2✔
666
                    Cache.GetSizeInfo(out _, out _, out long? bytesFree);
2✔
667

668
                    if (oldNumBytes > 0)
2!
669
                    {
×
670
                        switch (User.RaiseSelectionDialog(
×
671
                                    bytesFree.HasValue
672
                                        ? string.Format(Properties.Resources.GameInstanceManagerCacheMigrationPrompt,
673
                                                        CkanModule.FmtSize(oldNumBytes),
674
                                                        CkanModule.FmtSize(bytesFree.Value))
675
                                        : string.Format(Properties.Resources.GameInstanceManagerCacheMigrationPromptFreeSpaceUnknown,
676
                                                        CkanModule.FmtSize(oldNumBytes)),
677
                                    oldNumBytes < (bytesFree ?? 0) ? 0 : 2,
678
                                    Properties.Resources.GameInstanceManagerCacheMigrationMove,
679
                                    Properties.Resources.GameInstanceManagerCacheMigrationDelete,
680
                                    Properties.Resources.GameInstanceManagerCacheMigrationOpen,
681
                                    Properties.Resources.GameInstanceManagerCacheMigrationNothing,
682
                                    Properties.Resources.GameInstanceManagerCacheMigrationRevert))
683
                        {
684
                            case 0:
685
                                if (oldNumBytes < bytesFree)
×
686
                                {
×
687
                                    Cache.MoveFrom(new DirectoryInfo(origPath), progress);
×
688
                                    CacheChanged?.Invoke(origCache);
×
NEW
689
                                    origCache.Dispose();
×
UNCOV
690
                                }
×
691
                                else
692
                                {
×
693
                                    User.RaiseError(Properties.Resources.GameInstanceManagerCacheMigrationNotEnoughFreeSpace);
×
694
                                    // Abort since the user picked an option that doesn't work
695
                                    Cache = origCache;
×
696
                                    Configuration.DownloadCacheDir = origPath;
×
697
                                    failureReason = "";
×
698
                                }
×
699
                                break;
×
700

701
                            case 1:
702
                                origCache.RemoveAll();
×
703
                                CacheChanged?.Invoke(origCache);
×
NEW
704
                                origCache.Dispose();
×
UNCOV
705
                                break;
×
706

707
                            case 2:
708
                                Utilities.OpenFileBrowser(origPath);
×
NEW
709
                                Utilities.OpenFileBrowser(Configuration.DownloadCacheDir ?? DefaultDownloadCacheDir);
×
710
                                CacheChanged?.Invoke(origCache);
×
NEW
711
                                origCache.Dispose();
×
UNCOV
712
                                break;
×
713

714
                            case 3:
715
                                CacheChanged?.Invoke(origCache);
×
NEW
716
                                origCache.Dispose();
×
UNCOV
717
                                break;
×
718

719
                            case -1:
720
                            case 4:
721
                                Cache = origCache;
×
722
                                Configuration.DownloadCacheDir = origPath;
×
723
                                failureReason = "";
×
724
                                return false;
×
725
                        }
726
                    }
×
727
                    else
728
                    {
2✔
729
                        CacheChanged?.Invoke(origCache);
2!
730
                        origCache.Dispose();
2✔
731
                    }
2✔
732
                }
2✔
733
                failureReason = null;
2✔
734
                return true;
2✔
735
            }
736
            catch (DirectoryNotFoundKraken)
×
737
            {
×
738
                Cache = origCache;
×
739
                Configuration.DownloadCacheDir = origPath;
×
740
                failureReason = string.Format(Properties.Resources.GameInstancePathNotFound, path);
×
741
                return false;
×
742
            }
743
            catch (Exception ex)
×
744
            {
×
745
                Cache = origCache;
×
746
                Configuration.DownloadCacheDir = origPath;
×
747
                failureReason = ex.Message;
×
748
                return false;
×
749
            }
750
        }
2✔
751

752
        /// <summary>
753
        /// Releases all resource used by the <see cref="GameInstance"/> object.
754
        /// </summary>
755
        /// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="GameInstance"/>. The <see cref="Dispose"/>
756
        /// method leaves the <see cref="GameInstance"/> in an unusable state. After calling <see cref="Dispose"/>, you must
757
        /// release all references to the <see cref="GameInstance"/> so the garbage collector can reclaim the memory that
758
        /// the <see cref="GameInstance"/> was occupying.</remarks>
759
        public void Dispose()
760
        {
2✔
761
            Cache?.Dispose();
2!
762
            Cache = null;
2✔
763

764
            // Attempting to dispose of the related RegistryManager object here is a bad idea, it cause loads of failures
765
            GC.SuppressFinalize(this);
2✔
766
        }
2✔
767

768
        public static bool IsGameInstanceDir(DirectoryInfo path)
769
            => KnownGames.knownGames.Any(g => g.GameInFolder(path));
×
770

771
        /// <summary>
772
        /// Tries to determine the game that is installed at the given path
773
        /// </summary>
774
        /// <param name="path">A DirectoryInfo of the path to check</param>
775
        /// <param name="user">IUser object for interaction</param>
776
        /// <returns>An instance of the matching game or null if the user cancelled</returns>
777
        /// <exception cref="NotKSPDirKraken">Thrown when no games found</exception>
778
        public IGame? DetermineGame(DirectoryInfo path, IUser user)
779
        {
2✔
780
            var matchingGames = KnownGames.knownGames.Where(g => g.GameInFolder(path)).ToList();
2✔
781
            switch (matchingGames.Count)
2!
782
            {
783
                case 0:
784
                    throw new NotKSPDirKraken(path.FullName);
×
785

786
                case 1:
787
                    return matchingGames.First();
2✔
788

789
                default:
790
                    // Prompt user to choose
791
                    int selection = user.RaiseSelectionDialog(
×
792
                        string.Format(Properties.Resources.GameInstanceManagerSelectGamePrompt,
793
                                      Platform.FormatPath(path.FullName)),
794
                        matchingGames.Select(g => g.ShortName).ToArray());
×
795
                    return selection >= 0 ? matchingGames[selection] : null;
×
796
            }
797
        }
2✔
798

799
        public static readonly string DefaultDownloadCacheDir =
2✔
800
            Path.Combine(CKANPathUtils.AppDataPath, "downloads");
801
    }
802
}
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