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

KSP-CKAN / CKAN / 17904669173

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

push

github

HebaruSan
Merge #4443 Report number of filtered files in install

5231 of 7236 branches covered (72.29%)

Branch coverage included in aggregate %.

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

35 existing lines in 7 files now uncovered.

11163 of 14448 relevant lines covered (77.26%)

1.58 hits per line

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

66.61
/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();
2!
41
        private SteamLibrary? steamLib;
42

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

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

47
        public string? AutoStartInstance
48
        {
49
            get => Configuration.AutoStartInstance != null
2!
50
                   && HasInstance(Configuration.AutoStartInstance)
51
                       ? Configuration.AutoStartInstance
52
                       : null;
53

54
            private set
55
            {
2✔
56
                if (value != null && !string.IsNullOrEmpty(value) && !HasInstance(value))
2!
57
                {
×
58
                    throw new InvalidGameInstanceKraken(value);
×
59
                }
60
                Configuration.AutoStartInstance = value;
2✔
61
            }
2✔
62
        }
63

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

66
        public GameInstanceManager(IUser          user,
2✔
67
                                   IConfiguration configuration,
68
                                   SteamLibrary?  steamLib = null)
69
        {
2✔
70
            User = user;
2✔
71
            Configuration = configuration;
2✔
72
            this.steamLib = steamLib;
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 game 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 game 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
            => CurrentInstance ??= _GetPreferredInstance();
2!
94

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

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

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

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

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

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

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

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

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

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

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

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

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

312
        /// <summary>
313
        /// Create a new fake game instance
314
        /// </summary>
315
        /// <param name="game">The game of the new instance.</param>
316
        /// <param name="newName">The name for the new instance.</param>
317
        /// <param name="newPath">The location of the new instance.</param>
318
        /// <param name="version">The version of the new instance. Should have a build number.</param>
319
        /// <param name="dlcs">The IDlcDetector implementations for the DLCs that should be faked and the requested dlc version as a dictionary.</param>
320
        /// <exception cref="InstanceNameTakenKraken">Thrown if the instance name is already in use.</exception>
321
        /// <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>
322
        public GameInstance FakeInstance(IGame game, string newName, string newPath, GameVersion version,
323
                                         Dictionary<IDlcDetector, GameVersion>? dlcs = null)
324
        {
2✔
325
            TxFileManager fileMgr = new TxFileManager();
2✔
326
            using (TransactionScope transaction = CkanTransaction.CreateTransactionScope())
2✔
327
            {
2✔
328
                if (HasInstance(newName))
2!
329
                {
×
330
                    throw new InstanceNameTakenKraken(newName);
×
331
                }
332

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

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

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

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

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

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

371
                // Create the needed folder structure and the readme.txt for DLCs that should be simulated.
372
                if (dlcs != null)
2✔
373
                {
2✔
374
                    foreach ((IDlcDetector dlcDetector, GameVersion dlcVersion) in dlcs)
5✔
375
                    {
2✔
376
                        if (!dlcDetector.AllowedOnBaseVersion(version))
2✔
377
                        {
2✔
378
                            throw new WrongGameVersionKraken(
2✔
379
                                version,
380
                                string.Format(Properties.Resources.GameInstanceFakeDLCNotAllowed,
381
                                    game.ShortName,
382
                                    dlcDetector.ReleaseGameVersion,
383
                                    dlcDetector.IdentifierBaseName));
384
                        }
385

386
                        string dlcDir = Path.Combine(newPath, dlcDetector.InstallPath());
2✔
387
                        fileMgr.CreateDirectory(dlcDir);
2✔
388
                        fileMgr.WriteAllText(
2✔
389
                            Path.Combine(dlcDir, "readme.txt"),
390
                            string.Format("Version {0}", dlcVersion));
391
                    }
2✔
392
                }
2✔
393

394
                // Add the new instance to the config
395
                GameInstance new_instance = new GameInstance(game, newPath, newName, User);
2✔
396
                AddInstance(new_instance);
2✔
397
                transaction.Complete();
2✔
398
                return new_instance;
2✔
399
            }
400
        }
2✔
401

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

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

425
            // Check if a name with the current timestamp is valid
426
            validName = name + " (" + DateTime.Now + ")";
×
427

428
            if (InstanceNameIsValid(validName))
×
429
            {
×
430
                return validName;
×
431
            }
432

433
            // Give up
434
            throw new Kraken(Properties.Resources.GameInstanceNoValidName);
×
435
        }
2✔
436

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

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

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

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

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

511
        public void SetCurrentInstanceByPath(string path)
512
        {
2✔
513
            if (InstanceAt(path) is GameInstance inst)
2!
514
            {
2✔
515
                SetCurrentInstance(inst);
2✔
516
            }
2✔
517
        }
2✔
518

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

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

555
        public bool HasInstance(string name)
556
            => instances.ContainsKey(name);
2✔
557

558
        public void ClearAutoStart()
559
        {
2✔
560
            Configuration.AutoStartInstance = null;
2✔
561
        }
2✔
562

563
        private void LoadInstances()
564
        {
2✔
565
            log.Info("Loading game instances");
2✔
566

567
            instances.Clear();
2✔
568

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

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

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

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

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

691
                            case 1:
692
                                origCache.RemoveAll();
×
693
                                CacheChanged?.Invoke(origCache);
×
694
                                origCache.Dispose();
×
695
                                break;
×
696

697
                            case 2:
698
                                Utilities.OpenFileBrowser(origPath);
×
699
                                Utilities.OpenFileBrowser(Configuration.DownloadCacheDir ?? DefaultDownloadCacheDir);
×
700
                                CacheChanged?.Invoke(origCache);
×
701
                                origCache.Dispose();
×
702
                                break;
×
703

704
                            case 3:
705
                                CacheChanged?.Invoke(origCache);
×
706
                                origCache.Dispose();
×
707
                                break;
×
708

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

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

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

758
        public static bool IsGameInstanceDir(DirectoryInfo path)
759
            => KnownGames.knownGames.Any(g => g.GameInFolder(path));
2✔
760

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

785
        public static readonly string DefaultDownloadCacheDir =
2✔
786
            Path.Combine(CKANPathUtils.AppDataPath, "downloads");
787
    }
788
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc