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

KSP-CKAN / CKAN / 20403951254

21 Dec 2025 03:20AM UTC coverage: 85.205% (-0.1%) from 85.303%
20403951254

push

github

HebaruSan
Merge #4473 Check free space before cloning

2004 of 2171 branches covered (92.31%)

Branch coverage included in aggregate %.

17 of 24 new or added lines in 7 files covered. (70.83%)

18 existing lines in 2 files now uncovered.

11927 of 14179 relevant lines covered (84.12%)

1.76 hits per line

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

86.09
/Core/GameInstance.cs
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Linq;
5
using System.Reflection;
6
using System.Text.RegularExpressions;
7
using System.Diagnostics;
8
using System.Diagnostics.CodeAnalysis;
9

10
using ChinhDo.Transactions;
11
using log4net;
12
using Newtonsoft.Json;
13

14
using CKAN.IO;
15
using CKAN.Configuration;
16
using CKAN.Games;
17
using CKAN.Versioning;
18
using CKAN.Extensions;
19

20
namespace CKAN
21
{
22
    /// <summary>
23
    /// Everything for dealing with a game folder.
24
    /// </summary>
25
    public class GameInstance : IEquatable<GameInstance>
26
    {
27
        #region Construction and Initialisation
28

29
        /// <summary>
30
        /// Returns a game instance object.
31
        /// Will initialise a CKAN instance in the game dir if it does not already exist.
32
        /// </summary>
33
        public GameInstance(IGame game, string gameDir, string name, IUser? user)
2✔
34
        {
2✔
35
            Game = game;
2✔
36
            Name = name;
2✔
37
            User = user ?? new NullUser();
2✔
38
            // Make sure our path is absolute and has normalised slashes.
39
            GameDir = CKANPathUtils.NormalizePath(Path.GetFullPath(gameDir));
2✔
40
            if (Platform.IsWindows)
2✔
41
            {
1✔
42
                // Normalized slashes are bad for pure drive letters,
43
                // Path.Combine turns them into drive-relative paths like
44
                // K:GameData/whatever
45
                if (Regex.IsMatch(GameDir, @"^[a-zA-Z]:$"))
1✔
46
                {
×
47
                    GameDir = $"{GameDir}/";
×
48
                }
×
49
            }
1✔
50
            SetupCkanDirectories();
2✔
51
            LoadCompatibleVersions();
2✔
52
        }
2✔
53

54
        /// <returns>
55
        /// true if the game seems to be here and a version is found,
56
        /// false otherwise
57
        /// </returns>
58
        public bool Valid => Game.GameInFolder(new DirectoryInfo(GameDir)) && Version() != null;
2✔
59

60
        /// <returns>
61
        /// true if the instance may be locked, false otherwise.
62
        /// Note that this is a tentative value; if it's true,
63
        /// we still need to try to acquire the lock to confirm it isn't stale.
64
        /// </returns>
65
        public bool IsMaybeLocked => RegistryManager.IsInstanceMaybeLocked(CkanDir);
×
66

67
        /// <summary>
68
        /// Create the CKAN directory and any supporting files.
69
        /// </summary>
70
        [MemberNotNull(nameof(playTime))]
71
        private void SetupCkanDirectories()
72
        {
2✔
73
            log.InfoFormat("Initialising {0}", CkanDir);
2✔
74

75
            // TxFileManager knows if we are in a transaction
76
            var txFileMgr = new TxFileManager(CkanDir);
2✔
77

78
            if (!Directory.Exists(CkanDir))
2✔
79
            {
×
80
                User.RaiseMessage(Properties.Resources.GameInstanceSettingUp);
×
81
                User.RaiseMessage(Properties.Resources.GameInstanceCreatingDir, CkanDir);
×
82
                txFileMgr.CreateDirectory(CkanDir);
×
83
            }
×
84

85
            playTime = TimeLog.Load(TimeLog.GetPath(CkanDir)) ?? new TimeLog();
2✔
86

87
            if (!Directory.Exists(InstallHistoryDir))
2✔
88
            {
2✔
89
                User.RaiseMessage(Properties.Resources.GameInstanceCreatingDir, InstallHistoryDir);
2✔
90
                txFileMgr.CreateDirectory(InstallHistoryDir);
2✔
91
            }
2✔
92
            log.InfoFormat("Initialised {0}", CkanDir);
2✔
93
        }
2✔
94

95
        #endregion
96

97
        #region Fields and Properties
98

99
        public IUser User { get; private set; }
100

101
        public string Name { get; set; }
102

103
        /// <summary>
104
        /// Returns a file system safe version of the instance name that can be used within file names.
105
        /// </summary>
106
        public string SanitizedName => string.Join("", Name.Split(Path.GetInvalidFileNameChars()));
2✔
107

108
        public readonly string GameDir;
109
        public readonly IGame Game;
110
        private GameVersion? version;
111

112
        public TimeLog? playTime;
113

114
        public GameVersion? GameVersionWhenCompatibleVersionsWereStored
115
            => _compatibleVersions.GameVersionWhenWritten;
×
116

117
        public bool CompatibleVersionsAreFromDifferentGameVersion
118
            => GameVersionWhenCompatibleVersionsWereStored != Version();
×
119

120
        private CompatibleGameVersions _compatibleVersions;
121

122
        private static readonly ILog log = LogManager.GetLogger(typeof(GameInstance));
2✔
123

124
        public string CkanDir
125
            => ckanDir ??= CKANPathUtils.NormalizePath(Path.Combine(GameDir, "CKAN"));
2✔
126

127
        public string DownloadCacheDir
128
            => downloadDir ??= CKANPathUtils.NormalizePath(Path.Combine(CkanDir, "downloads"));
2✔
129

130
        public string InstallHistoryDir
131
            => historyDir ??= CKANPathUtils.NormalizePath(Path.Combine(CkanDir, "history"));
2✔
132

133
        private string? ckanDir;
134
        private string? downloadDir;
135
        private string? historyDir;
136

137
        public IOrderedEnumerable<FileInfo> InstallHistoryFiles()
138
            => Directory.EnumerateFiles(InstallHistoryDir, "*.ckan")
2✔
139
                        .Select(f => new FileInfo(f))
2✔
140
                        .OrderByDescending(fi => fi.CreationTime);
2✔
141

142
        public GameVersion? Version()
143
            => version ??= DetectVersion(GameDir);
2✔
144

145
        public GameVersionCriteria VersionCriteria()
146
            => new GameVersionCriteria(Version(), _compatibleVersions.Versions);
2✔
147

148
        #endregion
149

150
        #region Settings
151

152
        [MemberNotNull(nameof(_compatibleVersions))]
153
        private void LoadCompatibleVersions()
154
        {
2✔
155
            string path = CompatibleGameVersionsFile;
2✔
156
            if (File.Exists(path)
2✔
157
                && JsonConvert.DeserializeObject<CompatibleGameVersions>(File.ReadAllText(path))
158
                   is CompatibleGameVersions compatibleGameVersions)
159
            {
2✔
160
                _compatibleVersions = compatibleGameVersions;
2✔
161
            }
2✔
162
            else
163
            {
2✔
164
                _compatibleVersions = new CompatibleGameVersions()
2✔
165
                {
166
                    GameVersionWhenWritten = null,
167
                    Versions               = Version() is GameVersion gv
168
                                                 ? Game.DefaultCompatibleVersions(gv).ToList()
169
                                                 : new List<GameVersion>(),
170
                };
171
            }
2✔
172
        }
2✔
173

174
        [MemberNotNull(nameof(_compatibleVersions))]
175
        public void SetCompatibleVersions(IReadOnlyCollection<GameVersion> compatibleVersions)
176
        {
2✔
177
            _compatibleVersions = new CompatibleGameVersions()
2✔
178
            {
179
                GameVersionWhenWritten = Version(),
180
                Versions               = compatibleVersions.Distinct()
181
                                                           .OrderDescending()
182
                                                           .ToList(),
183
            };
184
            JsonConvert.SerializeObject(_compatibleVersions)
2✔
185
                       .WriteThroughTo(CompatibleGameVersionsFile);
186
        }
2✔
187

188
        private string CompatibleGameVersionsFile
189
            => Path.Combine(CkanDir, Game.CompatibleVersionsFile);
2✔
190

191
        public IReadOnlyCollection<GameVersion> CompatibleVersions => _compatibleVersions.Versions;
2✔
192

193
        public HashSet<string> GetSuppressedCompatWarningIdentifiers
194
            => SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile)
2✔
195
                                                 .Identifiers;
196

197
        public void AddSuppressedCompatWarningIdentifiers(HashSet<string> idents)
198
        {
2✔
199
            var scwi = SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile);
2✔
200
            scwi.Identifiers.UnionWith(idents);
2✔
201
            scwi.SaveTo(SuppressedCompatWarningIdentifiersFile);
2✔
202
        }
2✔
203

204
        private string SuppressedCompatWarningIdentifiersFile
205
            => Path.Combine(CkanDir, "suppressed_compat_warning_identifiers.json");
2✔
206

207
        public string[] InstallFilters
208
        {
209
            get => (File.Exists(InstallFiltersFile)
2✔
210
                        ? JsonConvert.DeserializeObject<string[]>(File.ReadAllText(InstallFiltersFile))
211
                        : null)
212
                   ?? Array.Empty<string>();
213

214
            #pragma warning disable IDE0027
215
            set
216
            {
2✔
217
                JsonConvert.SerializeObject(value)
2✔
218
                           .WriteThroughTo(InstallFiltersFile);
219
            }
2✔
220
            #pragma warning restore IDE0027
221
        }
222

223
        private string InstallFiltersFile => Path.Combine(CkanDir, "install_filters.json");
2✔
224

225
        public StabilityToleranceConfig StabilityToleranceConfig
226
            => stabilityToleranceConfig ??= new StabilityToleranceConfig(StabilityToleranceFile);
2✔
227

228
        private StabilityToleranceConfig? stabilityToleranceConfig;
229

230
        private string StabilityToleranceFile
231
            => Path.Combine(CkanDir, "stability_tolerance.json");
2✔
232

233
        #endregion
234

235
        #region Game Directory Detection and Versioning
236

237
        /// <summary>
238
        /// Returns the path to our portable version of game if ckan.exe is in the same
239
        /// directory as the game, or if the game is in the current directory.
240
        /// Otherwise, returns null.
241
        /// </summary>
242
        public static string? PortableDir(IGame game)
243
            => new string?[]
2✔
244
               {
245
                   Assembly.GetExecutingAssembly()?.Location,
246
                   Process.GetCurrentProcess()?.MainModule?.FileName,
247
               }
248
                   .OfType<string>()
249
                   .Select(Path.GetDirectoryName)
250
                   .OfType<string>()
251
                   .Prepend(Directory.GetCurrentDirectory())
252
                   .Select(path => new DirectoryInfo(path))
2✔
253
                   .FirstOrDefault(game.GameInFolder)
254
                   ?.FullName;
255

256
        /// <summary>
257
        /// Detects the version of a game in a given directory.
258
        /// </summary>
259
        private GameVersion? DetectVersion(string directory)
260
            => Game.DetectVersion(new DirectoryInfo(directory));
2✔
261

262
        #endregion
263

264
        /// <summary>
265
        /// Returns path relative to this instance's GameDir.
266
        /// </summary>
267
        public string ToRelativeGameDir(string path)
268
            => CKANPathUtils.ToRelative(path, GameDir);
2✔
269

270
        /// <summary>
271
        /// Given a path relative to this instance's GameDir, returns the
272
        /// absolute path on the system.
273
        /// </summary>
274
        public string ToAbsoluteGameDir(string path)
275
            => CKANPathUtils.ToAbsolute(path, GameDir);
2✔
276

277
        /// <summary>
278
        /// https://xkcd.com/208/
279
        /// This regex matches things like GameData/Foo/Foo.1.2.dll
280
        /// </summary>
281
        private static readonly Regex dllPattern = new Regex(
2✔
282
            @"
283
                ^(?:.*/)?             # Directories (ending with /)
284
                (?<identifier>[^.]+)  # Our DLL name, up until the first dot.
285
                .*\.dll$              # Everything else, ending in dll
286
            ",
287
            RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
288
        );
289

290
        /// <summary>
291
        /// Find the identifier associated with a manually installed DLL
292
        /// </summary>
293
        /// <param name="relPath">Path of the DLL relative to game root</param>
294
        /// <returns>
295
        /// Identifier if found otherwise null
296
        /// </returns>
297
        public string? DllPathToIdentifier(string relPath)
298
            => DllPathToIdentifier(Game, relPath);
2✔
299

300
        public static string? DllPathToIdentifier(IGame game, string relPath)
301
            // DLLs only live in the primary or alternate mod directories
302
            => game.AlternateModDirectoriesRelative
2✔
303
                   .Prepend(game.PrimaryModDirectoryRelative)
304
                   .Any(p => relPath.StartsWith($"{p}/", Platform.PathComparison))
2✔
305
               && dllPattern.Match(relPath) is { Success: true } match
306
                   ? Identifier.Sanitize(match.Groups["identifier"].Value)
307
                   : null;
308

309
        /// <summary>
310
        /// Generate a sequence of files in the game folder that weren't installed by CKAN
311
        /// </summary>
312
        /// <param name="registry">A Registry object that knows which files CKAN installed in this folder</param>
313
        /// <returns>Relative file paths as strings</returns>
314
        public IEnumerable<string> UnmanagedFiles(Registry registry)
315
            => Directory.EnumerateFiles(GameDir, "*", SearchOption.AllDirectories)
2✔
316
                        .Select(CKANPathUtils.NormalizePath)
317
                        .Where(absPath => !absPath.StartsWith(CkanDir))
2✔
318
                        .Select(ToRelativeGameDir)
319
                        .Where(relPath =>
320
                            !Game.StockFolders.Any(f => relPath.StartsWith($"{f}/"))
2✔
321
                            && registry.FileOwner(relPath) == null);
322

323
        /// <summary>
324
        /// Check whether a given path contains any files or folders installed by CKAN
325
        /// </summary>
326
        /// <param name="registry">A Registry object that knows which files CKAN installed in this folder</param>
327
        /// <param name="absPath">Absolute path to a folder to check</param>
328
        /// <returns>true if any descendants of given path were installed by CKAN, false otherwise</returns>
329
        public bool HasManagedFiles(Registry registry, string absPath)
330
            => registry.FileOwner(ToRelativeGameDir(absPath)) != null
2✔
331
               || (Directory.Exists(absPath)
332
                   && Directory.EnumerateFileSystemEntries(absPath, "*", SearchOption.AllDirectories)
333
                               .Any(f => registry.FileOwner(ToRelativeGameDir(f)) != null));
2✔
334

335
        /// <summary>
336
        /// Returns the number of bytes on disk consumed by this game instance
337
        /// </summary>
NEW
338
        public long TotalSize => Directory.EnumerateFiles(GameDir, "*", SearchOption.AllDirectories)
×
NEW
339
                                          .Sum(path => new FileInfo(path).Length);
×
340

341
        /// <summary>
342
        /// Returns the number of bytes needed to clone this game instance if hard links are allowed
343
        /// </summary>
344
        public long NonHardLinkableSize(string[] leaveEmpty)
345
            => Utilities.DirectoryNonHardLinkableSize(
2✔
346
                   new DirectoryInfo(GameDir),
347
                   new string[] { "CKAN/registry.locked", "CKAN/playtime.json" },
348
                   Platform.IsWindows ? Game.StockFolders
349
                                      : Array.Empty<string>(),
350
                   leaveEmpty,
351
                   new string[] { "CKAN" });
352

353
        [ExcludeFromCodeCoverage]
354
        public void PlayGame(string command, Action? onExit = null)
355
        {
356
            if (Game.AdjustCommandLine(command.Split(' '), Version())
357
                //is [string binary, ..] and string[] split
358
                is string[] split
359
                && split.Length > 0
360
                && split[0] is string binary)
361
            {
362
                try
363
                {
364
                    Directory.SetCurrentDirectory(GameDir);
365
                    Process p = new Process()
366
                    {
367
                        StartInfo = new ProcessStartInfo()
368
                        {
369
                            FileName  = binary,
370
                            Arguments = string.Join(" ", split.Skip(1))
371
                        },
372
                        EnableRaisingEvents = true
373
                    };
374

375
                    var isSteam = SteamLibrary.IsSteamCmdLine(command);
376
                    p.Exited += (sender, e) =>
377
                    {
378
                        if (!isSteam)
379
                        {
380
                            playTime?.Stop(CkanDir);
381
                        }
382
                        onExit?.Invoke();
383
                    };
384

385
                    p.Start();
386
                    if (!isSteam)
387
                    {
388
                        playTime?.Start();
389
                    }
390
                }
391
                catch (Exception exception)
392
                {
393
                    User.RaiseError(Properties.Resources.GameInstancePlayGameFailed, exception.Message);
394
                }
395
            }
396
        }
397

398
        public override string ToString()
399
            => string.Format(Properties.Resources.GameInstanceToString, Game.ShortName, GameDir);
×
400

401
        public bool Equals(GameInstance? other)
402
            => other != null && GameDir.Equals(other.GameDir);
2✔
403

404
        public override bool Equals(object? obj)
405
            => Equals(obj as GameInstance);
×
406

407
        public override int GetHashCode()
408
            => GameDir.GetHashCode();
×
409
    }
410
}
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