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

KSP-CKAN / CKAN / 18889376578

28 Oct 2025 09:13PM UTC coverage: 84.85% (+3.0%) from 81.873%
18889376578

Pull #4454

github

HebaruSan
Build on Windows, upload multi-platform coverage
Pull Request #4454: Build on Windows, upload multi-platform coverage

1974 of 2144 branches covered (92.07%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

97 existing lines in 25 files now uncovered.

11904 of 14212 relevant lines covered (83.76%)

0.88 hits per line

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

72.77
/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
using CKAN.DLC;
19

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

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

40
        public  SteamLibrary  SteamLibrary => steamLib ??= new SteamLibrary();
1✔
41
        private SteamLibrary? steamLib;
42

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

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

47
        public SortedList<string, GameInstance> Instances => new SortedList<string, GameInstance>(instances);
1✔
48

49
        public GameInstanceManager(IUser          user,
1✔
50
                                   IConfiguration configuration,
51
                                   SteamLibrary?  steamLib = null)
52
        {
1✔
53
            User = user;
1✔
54
            Configuration = configuration;
1✔
55
            this.steamLib = steamLib;
1✔
56
            LoadInstances();
1✔
57
            LoadCacheSettings();
1✔
58
        }
1✔
59

60
        /// <summary>
61
        /// Returns the preferred game instance, or null if none can be found.
62
        ///
63
        /// This works by checking to see if we're in a game dir first, then the
64
        /// config for an autostart instance, then will try to auto-populate
65
        /// by scanning for the game.
66
        ///
67
        /// This *will not* touch the config if we find a portable install.
68
        ///
69
        /// This *will* run game instance autodetection if the config is empty.
70
        ///
71
        /// This *will* set the current instance, or throw an exception if it's already set.
72
        ///
73
        /// Returns null if we have multiple instances, but none of them are preferred.
74
        /// </summary>
75
        public GameInstance? GetPreferredInstance()
76
            => CurrentInstance ??= _GetPreferredInstance();
1✔
77

78
        private GameInstance? _GetPreferredInstance()
79
        {
1✔
80
            // First check if we're part of a portable install
81
            // Note that this *does not* register in the config.
82
            switch (KnownGames.knownGames.Select(g => GameInstance.PortableDir(g)
1✔
83
                                                      is string p
84
                                                          ? new GameInstance(g, p,
85
                                                                             Properties.Resources.GameInstanceManagerPortable,
86
                                                                             User)
87
                                                          : null)
88
                                         .OfType<GameInstance>()
89
                                         .Where(i => i.Valid)
×
90
                                         .ToArray())
91
            {
92
                case { Length: 1 } insts:
93
                    return insts.Single();
×
94

95
                case { Length: > 1 } insts:
96
                    if (User.RaiseSelectionDialog(
×
97
                            string.Format(Properties.Resources.GameInstanceManagerSelectGamePrompt,
98
                                          string.Join(", ", insts.Select(i => i.GameDir)
×
99
                                                                 .Distinct()
100
                                                                 .Select(Platform.FormatPath))),
101
                            insts.Select(i => i.Game.ShortName)
×
102
                                 .ToArray())
103
                        is int selection and >= 0)
104
                    {
×
105
                        return insts[selection];
×
106
                    }
107
                    break;
×
108
            }
109

110
            // If we only know of a single instance, return that.
111
            if (instances.Count == 1
1✔
112
                && instances.Values.Single() is { Valid: true } and var inst)
113
            {
1✔
114
                return inst;
1✔
115
            }
116

117
            // Return the autostart, if we can find it.
118
            // We check both null and "" as we can't write NULL to the config, so we write an empty string instead
119
            // This is necessary so we can indicate that the user wants to reset the current AutoStartInstance without clearing the config!
120
            if (Configuration.AutoStartInstance is { Length: > 0 } instName
1✔
121
                && instances.TryGetValue(instName, out GameInstance? autoInst)
122
                && autoInst.Valid)
123
            {
×
124
                return autoInst;
×
125
            }
126

127
            // If we know of no instances, try to find one.
128
            // Otherwise, we know of too many instances!
129
            // We don't know which one to pick, so we return null.
130
            return instances.Count == 0 ? FindAndRegisterDefaultInstances() : null;
1✔
131
        }
1✔
132

133
        /// <summary>
134
        /// Find and register default instances by running
135
        /// game autodetection code. Registers one per known game,
136
        /// uses first found as default.
137
        ///
138
        /// Returns the resulting game instance if found.
139
        /// </summary>
140
        public GameInstance? FindAndRegisterDefaultInstances()
141
        {
1✔
142
            if (instances.Count != 0)
1✔
143
            {
×
144
                throw new GameManagerKraken("Attempted to scan for defaults with instances");
×
145
            }
146
            var found = FindDefaultInstances();
1✔
147
            foreach (var inst in found)
4✔
148
            {
1✔
149
                log.DebugFormat("Registering {0} at {1}...",
1✔
150
                                inst.Name, inst.GameDir);
151
                AddInstance(inst);
1✔
152
            }
1✔
153
            return found.FirstOrDefault();
1✔
154
        }
1✔
155

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

200
        /// <summary>
201
        /// Adds a game instance to config.
202
        /// </summary>
203
        /// <returns>The resulting GameInstance object</returns>
204
        /// <exception cref="NotGameDirKraken">Thrown if the instance is not a valid game instance.</exception>
205
        public GameInstance AddInstance(GameInstance instance)
206
        {
1✔
207
            if (instance.Valid)
1✔
208
            {
1✔
209
                string name = instance.Name;
1✔
210
                instances.Add(name, instance);
1✔
211
                Configuration.SetRegistryToInstances(instances);
1✔
212
            }
1✔
213
            else
214
            {
×
215
                throw new NotGameDirKraken(instance.GameDir);
×
216
            }
217
            return instance;
1✔
218
        }
1✔
219

220
        /// <summary>
221
        /// Adds a game instance to config.
222
        /// </summary>
223
        /// <param name="path">The path of the instance</param>
224
        /// <param name="name">The name of the instance</param>
225
        /// <param name="user">IUser object for interaction</param>
226
        /// <returns>The resulting GameInstance object</returns>
227
        /// <exception cref="NotGameDirKraken">Thrown if the instance is not a valid game instance.</exception>
228
        public GameInstance? AddInstance(string path, string name, IUser user)
229
        {
1✔
230
            var game = DetermineGame(new DirectoryInfo(path), user);
1✔
231
            return game == null ? null : AddInstance(new GameInstance(game, path, name, user));
1✔
232
        }
1✔
233

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

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

285
            log.Debug("Copying directory.");
1✔
286
            Utilities.CopyDirectory(existingInstance.GameDir, newPath,
1✔
287
                                    shareStockFolders ? existingInstance.Game.StockFolders
288
                                                      : Array.Empty<string>(),
289
                                    leaveEmpty);
290

291
            // Add the new instance to the config
292
            AddInstance(new GameInstance(existingInstance.Game, newPath, newName, User));
1✔
293
        }
1✔
294

295
        /// <summary>
296
        /// Create a new fake game instance
297
        /// </summary>
298
        /// <param name="game">The game of the new instance.</param>
299
        /// <param name="newName">The name for the new instance.</param>
300
        /// <param name="newPath">The location of the new instance.</param>
301
        /// <param name="version">The version of the new instance. Should have a build number.</param>
302
        /// <param name="dlcs">The IDlcDetector implementations for the DLCs that should be faked and the requested dlc version as a dictionary.</param>
303
        /// <exception cref="InstanceNameTakenKraken">Thrown if the instance name is already in use.</exception>
304
        /// <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>
305
        public GameInstance FakeInstance(IGame game, string newName, string newPath, GameVersion version,
306
                                         Dictionary<IDlcDetector, GameVersion>? dlcs = null)
307
        {
1✔
308
            TxFileManager fileMgr = new TxFileManager();
1✔
309
            using (TransactionScope transaction = CkanTransaction.CreateTransactionScope())
1✔
310
            {
1✔
311
                if (HasInstance(newName))
1✔
312
                {
1✔
313
                    throw new InstanceNameTakenKraken(newName);
1✔
314
                }
315

316
                if (!version.WithoutBuild.InBuildMap(game))
1✔
317
                {
1✔
318
                    throw new BadGameVersionKraken(string.Format(
1✔
319
                        Properties.Resources.GameInstanceFakeBadVersion, game.ShortName, version));
320
                }
321
                if (Directory.Exists(newPath) && (Directory.GetFiles(newPath).Length != 0 || Directory.GetDirectories(newPath).Length != 0))
1✔
322
                {
1✔
323
                    throw new BadInstallLocationKraken(Properties.Resources.GameInstanceFakeNotEmpty);
1✔
324
                }
325

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

328
                // Create a game root directory, containing a GameData folder, a buildID.txt/buildID64.txt and a readme.txt
329
                fileMgr.CreateDirectory(newPath);
1✔
330
                fileMgr.CreateDirectory(Path.Combine(newPath, game.PrimaryModDirectoryRelative));
1✔
331
                game.RebuildSubdirectories(newPath);
1✔
332

333
                foreach (var anchor in game.InstanceAnchorFiles)
4✔
334
                {
1✔
335
                    fileMgr.WriteAllText(Path.Combine(newPath, anchor),
1✔
336
                                         version.WithoutBuild.ToString());
337
                }
1✔
338

339
                // Don't write the buildID.txts if we have no build, otherwise it would be -1.
340
                if (version.IsBuildDefined && game is KerbalSpaceProgram)
1✔
341
                {
1✔
342
                    foreach (var b in KspBuildIdVersionProvider.buildIDfilenames)
4✔
343
                    {
1✔
344
                        fileMgr.WriteAllText(Path.Combine(newPath, b),
1✔
345
                                             string.Format("build id = {0}", version.Build));
346
                    }
1✔
347
                }
1✔
348

349
                // Create the readme.txt WITHOUT build number
350
                fileMgr.WriteAllText(Path.Combine(newPath, "readme.txt"),
1✔
351
                                     string.Format("Version {0}",
352
                                                   version.WithoutBuild.ToString()));
353

354
                // Create the needed folder structure and the readme.txt for DLCs that should be simulated.
355
                if (dlcs != null)
1✔
356
                {
1✔
357
                    foreach ((IDlcDetector dlcDetector, GameVersion dlcVersion) in dlcs)
4✔
358
                    {
1✔
359
                        if (!dlcDetector.AllowedOnBaseVersion(version))
1✔
360
                        {
1✔
361
                            throw new WrongGameVersionKraken(
1✔
362
                                version,
363
                                string.Format(Properties.Resources.GameInstanceFakeDLCNotAllowed,
364
                                    game.ShortName,
365
                                    dlcDetector.ReleaseGameVersion,
366
                                    dlcDetector.IdentifierBaseName));
367
                        }
368

369
                        string dlcDir = Path.Combine(newPath, dlcDetector.InstallPath());
1✔
370
                        fileMgr.CreateDirectory(dlcDir);
1✔
371
                        fileMgr.WriteAllText(
1✔
372
                            Path.Combine(dlcDir, "readme.txt"),
373
                            string.Format("Version {0}", dlcVersion));
374
                    }
1✔
375
                }
1✔
376

377
                // Add the new instance to the config
378
                GameInstance new_instance = new GameInstance(game, newPath, newName, User);
1✔
379
                AddInstance(new_instance);
1✔
380
                transaction.Complete();
1✔
381
                return new_instance;
1✔
382
            }
383
        }
1✔
384

385
        /// <summary>
386
        /// Given a string returns a unused valid instance name by postfixing the string
387
        /// </summary>
388
        /// <returns> A unused valid instance name.</returns>
389
        /// <param name="name">The name to use as a base.</param>
390
        /// <exception cref="Kraken">Could not find a valid name.</exception>
391
        public string GetNextValidInstanceName(string name)
392
        {
1✔
393
            // Check if the current name is valid
394
            if (InstanceNameIsValid(name))
1✔
395
            {
×
396
                return name;
×
397
            }
398

399
            // Try appending a number to the name
400
            var validName = Enumerable.Repeat(name, 1000)
1✔
401
                .Select((s, i) => s + " (" + i + ")")
1✔
402
                .FirstOrDefault(InstanceNameIsValid);
403
            if (validName != null)
1✔
404
            {
1✔
405
                return validName;
1✔
406
            }
407

408
            // Check if a name with the current timestamp is valid
409
            validName = name + " (" + DateTime.Now + ")";
×
410

411
            if (InstanceNameIsValid(validName))
×
412
            {
×
413
                return validName;
×
414
            }
415

416
            // Give up
417
            throw new Kraken(Properties.Resources.GameInstanceNoValidName);
×
418
        }
1✔
419

420
        /// <summary>
421
        /// Check if the instance name is valid.
422
        /// </summary>
423
        /// <returns><c>true</c>, if name is valid, <c>false</c> otherwise.</returns>
424
        /// <param name="name">Name to check.</param>
425
        private bool InstanceNameIsValid(string name)
426
        {
1✔
427
            // Discard null, empty strings and white space only strings.
428
            // Look for the current name in the list of loaded instances.
429
            return !string.IsNullOrWhiteSpace(name) && !HasInstance(name);
1✔
430
        }
1✔
431

432
        /// <summary>
433
        /// Removes the instance from the config and saves.
434
        /// </summary>
435
        public void RemoveInstance(string name)
436
        {
1✔
437
            instances.Remove(name);
1✔
438
            Configuration.SetRegistryToInstances(instances);
1✔
439
        }
1✔
440

441
        /// <summary>
442
        /// Renames an instance in the config and saves.
443
        /// </summary>
444
        public void RenameInstance(string from, string to)
445
        {
1✔
446
            if (from != to)
1✔
447
            {
1✔
448
                if (instances.ContainsKey(to))
1✔
449
                {
1✔
450
                    throw new InstanceNameTakenKraken(to);
1✔
451
                }
452
                var inst = instances[from];
1✔
453
                instances.Remove(from);
1✔
454
                inst.Name = to;
1✔
455
                instances.Add(to, inst);
1✔
456
                Configuration.SetRegistryToInstances(instances);
1✔
457
            }
1✔
458
        }
1✔
459

460
        /// <summary>
461
        /// Sets the current instance.
462
        /// Throws an InvalidGameInstanceKraken if not found.
463
        /// </summary>
464
        public void SetCurrentInstance(string name)
465
        {
1✔
466
            if (!instances.TryGetValue(name, out GameInstance? inst))
1✔
467
            {
1✔
468
                throw new InvalidGameInstanceKraken(name);
1✔
469
            }
470
            else if (!inst.Valid)
1✔
471
            {
×
472
                throw new NotGameDirKraken(inst.GameDir);
×
473
            }
474
            else
475
            {
1✔
476
                SetCurrentInstance(inst);
1✔
477
            }
1✔
478
        }
1✔
479

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

494
        public void SetCurrentInstanceByPath(string path)
495
        {
1✔
496
            if (InstanceAt(path) is GameInstance inst)
1✔
497
            {
1✔
498
                SetCurrentInstance(inst);
1✔
499
            }
1✔
500
        }
1✔
501

502
        public GameInstance? InstanceAt(string path)
503
        {
1✔
504
            var di = new DirectoryInfo(path);
1✔
505
            if (DetermineGame(di, User) is IGame game)
1✔
506
            {
1✔
507
                var inst = new GameInstance(game, path,
1✔
508
                                            Properties.Resources.GameInstanceByPathName,
509
                                            User);
510
                if (inst.Valid)
1✔
511
                {
1✔
512
                    return inst;
1✔
513
                }
514
                else
515
                {
×
516
                    throw new NotGameDirKraken(inst.GameDir);
×
517
                }
518
            }
519
            return null;
×
520
        }
1✔
521

522
        /// <summary>
523
        /// Sets the autostart instance in the config and saves it.
524
        /// </summary>
525
        public void SetAutoStart(string name)
526
        {
1✔
527
            if (!HasInstance(name))
1✔
528
            {
1✔
529
                throw new InvalidGameInstanceKraken(name);
1✔
530
            }
531
            else if (!instances[name].Valid)
1✔
532
            {
×
533
                throw new NotGameDirKraken(instances[name].GameDir);
×
534
            }
535
            Configuration.AutoStartInstance = name;
1✔
536
        }
1✔
537

538
        public bool HasInstance(string name)
539
            => instances.ContainsKey(name);
1✔
540

541
        public void ClearAutoStart()
542
        {
1✔
543
            Configuration.AutoStartInstance = null;
1✔
544
        }
1✔
545

546
        private void LoadInstances()
547
        {
1✔
548
            log.Info("Loading game instances");
1✔
549

550
            instances.Clear();
1✔
551

552
            foreach (Tuple<string, string, string> instance in Configuration.GetInstances())
4✔
553
            {
1✔
554
                var name = instance.Item1;
1✔
555
                var path = instance.Item2;
1✔
556
                var gameName = instance.Item3;
1✔
557
                try
558
                {
1✔
559
                    var game = KnownGames.knownGames.FirstOrDefault(g => g.ShortName == gameName)
1✔
560
                        ?? KnownGames.knownGames.First();
561
                    log.DebugFormat("Loading {0} from {1}", name, path);
1✔
562
                    // Add unconditionally, sort out invalid instances downstream
563
                    instances.Add(name, new GameInstance(game, path, name, User));
1✔
564
                }
1✔
565
                catch (Exception exc)
×
566
                {
×
567
                    // Skip malformed instances (e.g. empty path)
568
                    log.Error($"Failed to load game instance with name=\"{name}\" path=\"{path}\" game=\"{gameName}\"",
×
569
                        exc);
570
                }
×
571
            }
1✔
572
        }
1✔
573

574
        private void LoadCacheSettings()
575
        {
1✔
576
            if (Configuration.DownloadCacheDir != null
1✔
577
                && !Directory.Exists(Configuration.DownloadCacheDir))
578
            {
1✔
579
                try
580
                {
1✔
581
                    Directory.CreateDirectory(Configuration.DownloadCacheDir);
1✔
582
                }
1✔
583
                catch
×
584
                {
×
585
                    // Can't create the configured directory, try reverting it to the default
586
                    Configuration.DownloadCacheDir = null;
×
587
                    Directory.CreateDirectory(DefaultDownloadCacheDir);
×
588
                }
×
589
            }
1✔
590

UNCOV
591
            var progress = new ProgressImmediate<int>(p => {});
×
592
            if (!TrySetupCache(Configuration.DownloadCacheDir,
1✔
593
                               progress,
594
                               out string? failReason)
595
                && Configuration.DownloadCacheDir is { Length: > 0 })
596
            {
×
597
                log.ErrorFormat("Cache not found at configured path {0}: {1}",
×
598
                                Configuration.DownloadCacheDir, failReason ?? "");
599
                // Fall back to default path to minimize chance of ending up in an invalid state at startup
600
                TrySetupCache(null, progress, out _);
×
601
            }
×
602
        }
1✔
603

604
        /// <summary>
605
        /// Switch to using a download cache in a new location
606
        /// </summary>
607
        /// <param name="path">Location of folder for new cache</param>
608
        /// <param name="progress">Progress object to report progress to</param>
609
        /// <param name="failureReason">Contains a human readable failure message if the setup failed</param>
610
        /// <returns>
611
        /// true if successful, false otherwise
612
        /// </returns>
613
        public bool TrySetupCache(string?        path,
614
                                  IProgress<int> progress,
615
                                  [NotNullWhen(false)]
616
                                  out string?    failureReason)
617
        {
1✔
618
            var origPath  = Configuration.DownloadCacheDir;
1✔
619
            var origCache = Cache;
1✔
620
            try
621
            {
1✔
622
                if (path is { Length: > 0 })
1✔
623
                {
1✔
624
                    Cache = new NetModuleCache(this, path);
1✔
625
                    if (path != origPath)
1✔
626
                    {
1✔
627
                        Configuration.DownloadCacheDir = path;
1✔
628
                    }
1✔
629
                }
1✔
630
                else
631
                {
1✔
632
                    Directory.CreateDirectory(DefaultDownloadCacheDir);
1✔
633
                    Cache = new NetModuleCache(this, DefaultDownloadCacheDir);
1✔
634
                    Configuration.DownloadCacheDir = null;
1✔
635
                }
1✔
636
                if (origPath != null && origCache != null && path != origPath)
1✔
637
                {
1✔
638
                    origCache.GetSizeInfo(out _, out long oldNumBytes, out _);
1✔
639
                    Cache.GetSizeInfo(out _, out _, out long? bytesFree);
1✔
640

641
                    if (oldNumBytes > 0)
1✔
642
                    {
×
643
                        switch (User.RaiseSelectionDialog(
×
644
                                    bytesFree.HasValue
645
                                        ? string.Format(Properties.Resources.GameInstanceManagerCacheMigrationPrompt,
646
                                                        CkanModule.FmtSize(oldNumBytes),
647
                                                        CkanModule.FmtSize(bytesFree.Value))
648
                                        : string.Format(Properties.Resources.GameInstanceManagerCacheMigrationPromptFreeSpaceUnknown,
649
                                                        CkanModule.FmtSize(oldNumBytes)),
650
                                    oldNumBytes < (bytesFree ?? 0) ? 0 : 2,
651
                                    Properties.Resources.GameInstanceManagerCacheMigrationMove,
652
                                    Properties.Resources.GameInstanceManagerCacheMigrationDelete,
653
                                    Properties.Resources.GameInstanceManagerCacheMigrationOpen,
654
                                    Properties.Resources.GameInstanceManagerCacheMigrationNothing,
655
                                    Properties.Resources.GameInstanceManagerCacheMigrationRevert))
656
                        {
657
                            case 0:
658
                                if (oldNumBytes < bytesFree)
×
659
                                {
×
660
                                    Cache.MoveFrom(new DirectoryInfo(origPath), progress);
×
661
                                    CacheChanged?.Invoke(origCache);
×
662
                                    origCache.Dispose();
×
663
                                }
×
664
                                else
665
                                {
×
666
                                    User.RaiseError(Properties.Resources.GameInstanceManagerCacheMigrationNotEnoughFreeSpace);
×
667
                                    // Abort since the user picked an option that doesn't work
668
                                    Cache = origCache;
×
669
                                    Configuration.DownloadCacheDir = origPath;
×
670
                                    failureReason = "";
×
671
                                }
×
672
                                break;
×
673

674
                            case 1:
675
                                origCache.RemoveAll();
×
676
                                CacheChanged?.Invoke(origCache);
×
677
                                origCache.Dispose();
×
678
                                break;
×
679

680
                            case 2:
681
                                Utilities.OpenFileBrowser(origPath);
×
682
                                Utilities.OpenFileBrowser(Configuration.DownloadCacheDir ?? DefaultDownloadCacheDir);
×
683
                                CacheChanged?.Invoke(origCache);
×
684
                                origCache.Dispose();
×
685
                                break;
×
686

687
                            case 3:
688
                                CacheChanged?.Invoke(origCache);
×
689
                                origCache.Dispose();
×
690
                                break;
×
691

692
                            case -1:
693
                            case 4:
694
                                Cache = origCache;
×
695
                                Configuration.DownloadCacheDir = origPath;
×
696
                                failureReason = "";
×
697
                                return false;
×
698
                        }
699
                    }
×
700
                    else
701
                    {
1✔
702
                        CacheChanged?.Invoke(origCache);
1✔
703
                        origCache.Dispose();
1✔
704
                    }
1✔
705
                }
1✔
706
                failureReason = null;
1✔
707
                return true;
1✔
708
            }
709
            catch (DirectoryNotFoundKraken)
×
710
            {
×
711
                Cache = origCache;
×
712
                Configuration.DownloadCacheDir = origPath;
×
713
                failureReason = string.Format(Properties.Resources.GameInstancePathNotFound, path);
×
714
                return false;
×
715
            }
716
            catch (Exception ex)
×
717
            {
×
718
                Cache = origCache;
×
719
                Configuration.DownloadCacheDir = origPath;
×
720
                failureReason = ex.Message;
×
721
                return false;
×
722
            }
723
        }
1✔
724

725
        /// <summary>
726
        /// Releases all resource used by the <see cref="GameInstance"/> object.
727
        /// </summary>
728
        /// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="GameInstance"/>. The <see cref="Dispose"/>
729
        /// method leaves the <see cref="GameInstance"/> in an unusable state. After calling <see cref="Dispose"/>, you must
730
        /// release all references to the <see cref="GameInstance"/> so the garbage collector can reclaim the memory that
731
        /// the <see cref="GameInstance"/> was occupying.</remarks>
732
        public void Dispose()
733
        {
1✔
734
            Cache?.Dispose();
1✔
735
            Cache = null;
1✔
736

737
            // Attempting to dispose of the related RegistryManager object here is a bad idea, it cause loads of failures
738
            GC.SuppressFinalize(this);
1✔
739
        }
1✔
740

741
        public static bool IsGameInstanceDir(DirectoryInfo path)
742
            => KnownGames.knownGames.Any(g => g.GameInFolder(path));
1✔
743

744
        /// <summary>
745
        /// Tries to determine the game that is installed at the given path
746
        /// </summary>
747
        /// <param name="path">A DirectoryInfo of the path to check</param>
748
        /// <param name="user">IUser object for interaction</param>
749
        /// <returns>An instance of the matching game or null if the user cancelled</returns>
750
        /// <exception cref="NotGameDirKraken">Thrown when no games found</exception>
751
        public IGame? DetermineGame(DirectoryInfo path, IUser user)
752
            => KnownGames.knownGames.Where(g => g.GameInFolder(path))
1✔
753
                                    .ToArray()
754
               switch
×
755
               {
756
                   { Length: 0 }       => throw new NotGameDirKraken(path.FullName),
1✔
757
                   { Length: 1 } games => games.Single(),
1✔
758
                   var games => user.RaiseSelectionDialog(
×
759
                                    string.Format(Properties.Resources.GameInstanceManagerSelectGamePrompt,
760
                                                  Platform.FormatPath(path.FullName)),
761
                                    games.Select(g => g.ShortName)
×
762
                                         .ToArray())
763
                                is int selection and >= 0
764
                                    ? games[selection]
765
                                    : null
766
               };
767

768
        public static readonly string DefaultDownloadCacheDir =
1✔
769
            Path.Combine(CKANPathUtils.AppDataPath, "downloads");
770
    }
771
}
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