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

KSP-CKAN / CKAN / 17179122983

23 Aug 2025 07:06PM UTC coverage: 58.488% (+0.004%) from 58.484%
17179122983

push

github

HebaruSan
Merge #4420 More tests and fixes

4748 of 8436 branches covered (56.28%)

Branch coverage included in aggregate %.

70 of 132 new or added lines in 17 files covered. (53.03%)

446 existing lines in 11 files now uncovered.

10050 of 16865 relevant lines covered (59.59%)

1.22 hits per line

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

53.39
/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 game 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
×
NEW
47
                                                            .SelectMany(g => g.InstanceAnchorFiles)
×
48
                                                            .Distinct()
49
                                                            .ToArray();
50

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

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

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

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

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

96
        private GameInstance? _GetPreferredInstance()
97
        {
2✔
98
            // First check if we're part of a portable install
99
            // Note that this *does not* register in the config.
100
            switch (KnownGames.knownGames.Select(g => GameInstance.PortableDir(g)
2!
101
                                                      is string p
102
                                                          ? new GameInstance(g, p,
103
                                                                             Properties.Resources.GameInstanceManagerPortable,
104
                                                                             User)
105
                                                          : null)
106
                                         .OfType<GameInstance>()
NEW
107
                                         .Where(i => i.Valid)
×
108
                                         .ToArray())
109
            {
110
                case { Length: 1 } insts:
NEW
111
                    return insts.Single();
×
112

113
                case { Length: > 1 } insts:
NEW
114
                    if (User.RaiseSelectionDialog(
×
115
                            string.Format(Properties.Resources.GameInstanceManagerSelectGamePrompt,
NEW
116
                                          string.Join(", ", insts.Select(i => i.GameDir())
×
117
                                                                 .Distinct()
118
                                                                 .Select(Platform.FormatPath))),
NEW
119
                            insts.Select(i => i.game.ShortName)
×
120
                                 .ToArray())
121
                        is int selection and >= 0)
UNCOV
122
                    {
×
NEW
123
                        return insts[selection];
×
124
                    }
NEW
125
                    break;
×
126
            }
127

128
            // If we only know of a single instance, return that.
129
            if (instances.Count == 1
2✔
130
                && instances.Values.Single() is { Valid: true } and var inst)
131
            {
2✔
132
                return inst;
2✔
133
            }
134

135
            // Return the autostart, if we can find it.
136
            // We check both null and "" as we can't write NULL to the config, so we write an empty string instead
137
            // This is necessary so we can indicate that the user wants to reset the current AutoStartInstance without clearing the config!
138
            if (AutoStartInstance is { Length: > 0 }
2!
139
                && instances.TryGetValue(AutoStartInstance, out GameInstance? autoInst)
140
                && autoInst.Valid)
UNCOV
141
            {
×
NEW
142
                return autoInst;
×
143
            }
144

145
            // If we know of no instances, try to find one.
146
            // Otherwise, we know of too many instances!
147
            // We don't know which one to pick, so we return null.
148
            return instances.Count == 0 ? FindAndRegisterDefaultInstances() : null;
2!
149
        }
2✔
150

151
        /// <summary>
152
        /// Find and register default instances by running
153
        /// game autodetection code. Registers one per known game,
154
        /// uses first found as default.
155
        ///
156
        /// Returns the resulting game instance if found.
157
        /// </summary>
158
        public GameInstance? FindAndRegisterDefaultInstances()
UNCOV
159
        {
×
UNCOV
160
            if (instances.Count != 0)
×
UNCOV
161
            {
×
NEW
162
                throw new GameManagerKraken("Attempted to scan for defaults with instances");
×
163
            }
UNCOV
164
            var found = FindDefaultInstances();
×
UNCOV
165
            foreach (var inst in found)
×
UNCOV
166
            {
×
UNCOV
167
                log.DebugFormat("Registering {0} at {1}...",
×
168
                                inst.Name, inst.GameDir());
169
                AddInstance(inst);
×
170
            }
×
171
            return found.FirstOrDefault();
×
UNCOV
172
        }
×
173

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

218
        /// <summary>
219
        /// Adds a game instance to config.
220
        /// </summary>
221
        /// <returns>The resulting GameInstance object</returns>
222
        /// <exception cref="NotGameDirKraken">Thrown if the instance is not a valid game instance.</exception>
223
        public GameInstance AddInstance(GameInstance instance)
224
        {
2✔
225
            if (instance.Valid)
2!
226
            {
2✔
227
                string name = instance.Name;
2✔
228
                instances.Add(name, instance);
2✔
229
                Configuration.SetRegistryToInstances(instances);
2✔
230
            }
2✔
231
            else
UNCOV
232
            {
×
NEW
233
                throw new NotGameDirKraken(instance.GameDir());
×
234
            }
235
            return instance;
2✔
236
        }
2✔
237

238
        /// <summary>
239
        /// Adds a game instance to config.
240
        /// </summary>
241
        /// <param name="path">The path of the instance</param>
242
        /// <param name="name">The name of the instance</param>
243
        /// <param name="user">IUser object for interaction</param>
244
        /// <returns>The resulting GameInstance object</returns>
245
        /// <exception cref="NotGameDirKraken">Thrown if the instance is not a valid game instance.</exception>
246
        public GameInstance? AddInstance(string path, string name, IUser user)
247
        {
2✔
248
            var game = DetermineGame(new DirectoryInfo(path), user);
2✔
249
            return game == null ? null : AddInstance(new GameInstance(game, path, name, user));
2!
250
        }
2✔
251

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

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

303
            log.Debug("Copying directory.");
2✔
304
            Utilities.CopyDirectory(existingInstance.GameDir(), newPath,
2✔
305
                                    shareStockFolders ? existingInstance.game.StockFolders
306
                                                      : Array.Empty<string>(),
307
                                    leaveEmpty);
308

309
            // Add the new instance to the config
310
            AddInstance(new GameInstance(existingInstance.game, newPath, newName, User));
2✔
311
        }
2✔
312

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

334
                if (!version.InBuildMap(game))
2✔
335
                {
2✔
336
                    throw new BadGameVersionKraken(string.Format(
2✔
337
                        Properties.Resources.GameInstanceFakeBadVersion, game.ShortName, version));
338
                }
339
                if (Directory.Exists(newPath) && (Directory.GetFiles(newPath).Length != 0 || Directory.GetDirectories(newPath).Length != 0))
2✔
340
                {
2✔
341
                    throw new BadInstallLocationKraken(Properties.Resources.GameInstanceFakeNotEmpty);
2✔
342
                }
343

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

346
                // Create a game root directory, containing a GameData folder, a buildID.txt/buildID64.txt and a readme.txt
347
                fileMgr.CreateDirectory(newPath);
2✔
348
                fileMgr.CreateDirectory(Path.Combine(newPath, game.PrimaryModDirectoryRelative));
2✔
349
                game.RebuildSubdirectories(newPath);
2✔
350

351
                foreach (var anchor in game.InstanceAnchorFiles)
5✔
352
                {
2✔
353
                    fileMgr.WriteAllText(Path.Combine(newPath, anchor),
2✔
354
                                         version.WithoutBuild.ToString());
355
                }
2✔
356

357
                // Don't write the buildID.txts if we have no build, otherwise it would be -1.
358
                if (version.IsBuildDefined && game is KerbalSpaceProgram)
2!
UNCOV
359
                {
×
UNCOV
360
                    foreach (var b in KspBuildIdVersionProvider.buildIDfilenames)
×
UNCOV
361
                    {
×
UNCOV
362
                        fileMgr.WriteAllText(Path.Combine(newPath, b),
×
363
                                             string.Format("build id = {0}", version.Build));
UNCOV
364
                    }
×
UNCOV
365
                }
×
366

367
                // Create the readme.txt WITHOUT build number
368
                fileMgr.WriteAllText(Path.Combine(newPath, "readme.txt"),
2✔
369
                                     string.Format("Version {0}",
370
                                                   version.WithoutBuild.ToString()));
371

372
                // Create the needed folder structure and the readme.txt for DLCs that should be simulated.
373
                if (dlcs != null)
2✔
374
                {
2✔
375
                    foreach (KeyValuePair<DLC.IDlcDetector, GameVersion> dlc in dlcs)
5✔
376
                    {
2✔
377
                        DLC.IDlcDetector dlcDetector = dlc.Key;
2✔
378
                        GameVersion dlcVersion = dlc.Value;
2✔
379

380
                        if (!dlcDetector.AllowedOnBaseVersion(version))
2✔
381
                        {
2✔
382
                            throw new WrongGameVersionKraken(
2✔
383
                                version,
384
                                string.Format(Properties.Resources.GameInstanceFakeDLCNotAllowed,
385
                                    game.ShortName,
386
                                    dlcDetector.ReleaseGameVersion,
387
                                    dlcDetector.IdentifierBaseName));
388
                        }
389

390
                        string dlcDir = Path.Combine(newPath, dlcDetector.InstallPath());
2✔
391
                        fileMgr.CreateDirectory(dlcDir);
2✔
392
                        fileMgr.WriteAllText(
2✔
393
                            Path.Combine(dlcDir, "readme.txt"),
394
                            string.Format("Version {0}", dlcVersion));
395
                    }
2✔
396
                }
2✔
397

398
                // Add the new instance to the config
399
                GameInstance new_instance = new GameInstance(game, newPath, newName, User);
2✔
400
                AddInstance(new_instance);
2✔
401
                transaction.Complete();
2✔
402
            }
2✔
403
        }
2✔
404

405
        /// <summary>
406
        /// Given a string returns a unused valid instance name by postfixing the string
407
        /// </summary>
408
        /// <returns> A unused valid instance name.</returns>
409
        /// <param name="name">The name to use as a base.</param>
410
        /// <exception cref="Kraken">Could not find a valid name.</exception>
411
        public string GetNextValidInstanceName(string name)
412
        {
2✔
413
            // Check if the current name is valid
414
            if (InstanceNameIsValid(name))
2!
UNCOV
415
            {
×
UNCOV
416
                return name;
×
417
            }
418

419
            // Try appending a number to the name
420
            var validName = Enumerable.Repeat(name, 1000)
2✔
421
                .Select((s, i) => s + " (" + i + ")")
2✔
422
                .FirstOrDefault(InstanceNameIsValid);
423
            if (validName != null)
2!
424
            {
2✔
425
                return validName;
2✔
426
            }
427

428
            // Check if a name with the current timestamp is valid
UNCOV
429
            validName = name + " (" + DateTime.Now + ")";
×
430

UNCOV
431
            if (InstanceNameIsValid(validName))
×
UNCOV
432
            {
×
UNCOV
433
                return validName;
×
434
            }
435

436
            // Give up
UNCOV
437
            throw new Kraken(Properties.Resources.GameInstanceNoValidName);
×
438
        }
2✔
439

440
        /// <summary>
441
        /// Check if the instance name is valid.
442
        /// </summary>
443
        /// <returns><c>true</c>, if name is valid, <c>false</c> otherwise.</returns>
444
        /// <param name="name">Name to check.</param>
445
        private bool InstanceNameIsValid(string name)
446
        {
2✔
447
            // Discard null, empty strings and white space only strings.
448
            // Look for the current name in the list of loaded instances.
449
            return !string.IsNullOrWhiteSpace(name) && !HasInstance(name);
2!
450
        }
2✔
451

452
        /// <summary>
453
        /// Removes the instance from the config and saves.
454
        /// </summary>
455
        public void RemoveInstance(string name)
456
        {
2✔
457
            instances.Remove(name);
2✔
458
            Configuration.SetRegistryToInstances(instances);
2✔
459
        }
2✔
460

461
        /// <summary>
462
        /// Renames an instance in the config and saves.
463
        /// </summary>
464
        public void RenameInstance(string from, string to)
465
        {
2✔
466
            if (from != to)
2!
467
            {
2✔
468
                if (instances.ContainsKey(to))
2!
NEW
469
                {
×
NEW
470
                    throw new InstanceNameTakenKraken(to);
×
471
                }
472
                var inst = instances[from];
2✔
473
                instances.Remove(from);
2✔
474
                inst.Name = to;
2✔
475
                instances.Add(to, inst);
2✔
476
                Configuration.SetRegistryToInstances(instances);
2✔
477
            }
2✔
478
        }
2✔
479

480
        /// <summary>
481
        /// Sets the current instance.
482
        /// Throws an InvalidGameInstanceKraken if not found.
483
        /// </summary>
484
        public void SetCurrentInstance(string name)
485
        {
2✔
486
            if (!instances.TryGetValue(name, out GameInstance? inst))
2✔
487
            {
2✔
488
                throw new InvalidGameInstanceKraken(name);
2✔
489
            }
490
            else if (!inst.Valid)
2!
UNCOV
491
            {
×
NEW
492
                throw new NotGameDirKraken(inst.GameDir());
×
493
            }
494
            else
495
            {
2✔
496
                SetCurrentInstance(inst);
2✔
497
            }
2✔
498
        }
2✔
499

500
        public void SetCurrentInstance(GameInstance? instance)
501
        {
2✔
502
            var prev = CurrentInstance;
2✔
503
            // Don't try to Dispose a null CurrentInstance.
504
            if (prev != null && !prev.Equals(instance))
2✔
505
            {
2✔
506
                // Dispose of the old registry manager to release the registry
507
                // (without accidentally locking/loading/etc it).
508
                RegistryManager.DisposeInstance(prev);
2✔
509
            }
2✔
510
            CurrentInstance = instance;
2✔
511
            InstanceChanged?.Invoke(prev, instance);
2!
512
        }
2✔
513

514
        public void SetCurrentInstanceByPath(string path)
UNCOV
515
        {
×
NEW
516
            if (InstanceAt(path) is GameInstance inst)
×
UNCOV
517
            {
×
NEW
518
                SetCurrentInstance(inst);
×
UNCOV
519
            }
×
520
        }
×
521

522
        public GameInstance? InstanceAt(string path)
UNCOV
523
        {
×
NEW
524
            var di = new DirectoryInfo(path);
×
NEW
525
            if (DetermineGame(di, User) is IGame game)
×
526
            {
×
NEW
527
                var inst = new GameInstance(game, path,
×
528
                                            Properties.Resources.GameInstanceByPathName,
529
                                            User);
NEW
530
                if (inst.Valid)
×
NEW
531
                {
×
NEW
532
                    return inst;
×
533
                }
534
                else
NEW
535
                {
×
NEW
536
                    throw new NotGameDirKraken(inst.GameDir());
×
537
                }
538
            }
NEW
539
            return null;
×
UNCOV
540
        }
×
541

542
        /// <summary>
543
        /// Sets the autostart instance in the config and saves it.
544
        /// </summary>
545
        public void SetAutoStart(string name)
546
        {
2✔
547
            if (!HasInstance(name))
2✔
548
            {
2✔
549
                throw new InvalidGameInstanceKraken(name);
2✔
550
            }
551
            else if (!instances[name].Valid)
2!
UNCOV
552
            {
×
NEW
553
                throw new NotGameDirKraken(instances[name].GameDir());
×
554
            }
555
            AutoStartInstance = name;
2✔
556
        }
2✔
557

558
        public bool HasInstance(string name)
559
            => instances.ContainsKey(name);
2✔
560

561
        public void ClearAutoStart()
562
        {
2✔
563
            Configuration.AutoStartInstance = null;
2✔
564
        }
2✔
565

566
        private void LoadInstances()
567
        {
2✔
568
            log.Info("Loading game instances");
2✔
569

570
            instances.Clear();
2✔
571

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

594
        private void LoadCacheSettings()
595
        {
2✔
596
            if (Configuration.DownloadCacheDir != null
2!
597
                && !Directory.Exists(Configuration.DownloadCacheDir))
UNCOV
598
            {
×
599
                try
600
                {
×
601
                    Directory.CreateDirectory(Configuration.DownloadCacheDir);
×
602
                }
×
UNCOV
603
                catch
×
UNCOV
604
                {
×
605
                    // Can't create the configured directory, try reverting it to the default
UNCOV
606
                    Configuration.DownloadCacheDir = null;
×
UNCOV
607
                    Directory.CreateDirectory(DefaultDownloadCacheDir);
×
UNCOV
608
                }
×
609
            }
×
610

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

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

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

694
                            case 1:
695
                                origCache.RemoveAll();
×
696
                                CacheChanged?.Invoke(origCache);
×
697
                                origCache.Dispose();
×
698
                                break;
×
699

700
                            case 2:
701
                                Utilities.OpenFileBrowser(origPath);
×
702
                                Utilities.OpenFileBrowser(Configuration.DownloadCacheDir ?? DefaultDownloadCacheDir);
×
703
                                CacheChanged?.Invoke(origCache);
×
UNCOV
704
                                origCache.Dispose();
×
UNCOV
705
                                break;
×
706

707
                            case 3:
708
                                CacheChanged?.Invoke(origCache);
×
709
                                origCache.Dispose();
×
710
                                break;
×
711

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

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

757
            // Attempting to dispose of the related RegistryManager object here is a bad idea, it cause loads of failures
758
            GC.SuppressFinalize(this);
2✔
759
        }
2✔
760

761
        public static bool IsGameInstanceDir(DirectoryInfo path)
UNCOV
762
            => KnownGames.knownGames.Any(g => g.GameInFolder(path));
×
763

764
        /// <summary>
765
        /// Tries to determine the game that is installed at the given path
766
        /// </summary>
767
        /// <param name="path">A DirectoryInfo of the path to check</param>
768
        /// <param name="user">IUser object for interaction</param>
769
        /// <returns>An instance of the matching game or null if the user cancelled</returns>
770
        /// <exception cref="NotGameDirKraken">Thrown when no games found</exception>
771
        public IGame? DetermineGame(DirectoryInfo path, IUser user)
772
            => KnownGames.knownGames.Where(g => g.GameInFolder(path))
2!
773
                                    .ToArray()
NEW
774
               switch
×
775
               {
NEW
776
                   { Length: 0 }       => throw new NotGameDirKraken(path.FullName),
×
777
                   { Length: 1 } games => games.Single(),
2✔
NEW
778
                   var games => user.RaiseSelectionDialog(
×
779
                                    string.Format(Properties.Resources.GameInstanceManagerSelectGamePrompt,
780
                                                  Platform.FormatPath(path.FullName)),
NEW
781
                                    games.Select(g => g.ShortName)
×
782
                                         .ToArray())
783
                                is int selection and >= 0
784
                                    ? games[selection]
785
                                    : null
786
               };
787

788
        public static readonly string DefaultDownloadCacheDir =
2✔
789
            Path.Combine(CKANPathUtils.AppDataPath, "downloads");
790
    }
791
}
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