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

KSP-CKAN / CKAN / 17179122983

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

push

github

HebaruSan
Merge #4420 More tests and fixes

4748 of 8436 branches covered (56.28%)

Branch coverage included in aggregate %.

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

446 existing lines in 11 files now uncovered.

10050 of 16865 relevant lines covered (59.59%)

1.22 hits per line

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

53.95
/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.FileManager;
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

23
    /// <summary>
24
    /// Everything for dealing with a game folder.
25
    /// </summary>
26
    public class GameInstance : IEquatable<GameInstance>
27
    {
28
        #region Fields and Properties
29

30
        public IUser User { get; private set; }
31

32
        private readonly string gameDir;
33
        public readonly IGame game;
34
        private GameVersion? version;
35
        private List<GameVersion> _compatibleVersions = new List<GameVersion>();
2✔
36

37
        public TimeLog? playTime;
38

39
        public string Name { get; set; }
40

41
        /// <summary>
42
        /// Returns a file system safe version of the instance name that can be used within file names.
43
        /// </summary>
44
        public string SanitizedName => string.Join("", Name.Split(Path.GetInvalidFileNameChars()));
2✔
45
        public GameVersion? GameVersionWhenCompatibleVersionsWereStored { get; private set; }
46
        public bool CompatibleVersionsAreFromDifferentGameVersion
47
            => _compatibleVersions.Count > 0
×
48
               && GameVersionWhenCompatibleVersionsWereStored != Version();
49

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

52
        #endregion
53

54
        #region Construction and Initialisation
55

56
        /// <summary>
57
        /// Returns a game instance object.
58
        /// Will initialise a CKAN instance in the game dir if it does not already exist,
59
        /// if the directory contains a valid game install.
60
        /// </summary>
61
        public GameInstance(IGame game, string gameDir, string name, IUser? user)
2✔
62
        {
2✔
63
            this.game = game;
2✔
64
            Name = name;
2✔
65
            User = user ?? new NullUser();
2✔
66
            // Make sure our path is absolute and has normalised slashes.
67
            this.gameDir = CKANPathUtils.NormalizePath(Path.GetFullPath(gameDir));
2✔
68
            if (Platform.IsWindows)
2!
69
            {
×
70
                // Normalized slashes are bad for pure drive letters,
71
                // Path.Combine turns them into drive-relative paths like
72
                // K:GameData/whatever
73
                if (Regex.IsMatch(this.gameDir, @"^[a-zA-Z]:$"))
×
74
                {
×
75
                    this.gameDir = $"{this.gameDir}/";
×
76
                }
×
77
            }
×
78
            if (Valid)
2✔
79
            {
2✔
80
                SetupCkanDirectories();
2✔
81
                LoadCompatibleVersions();
2✔
82
            }
2✔
83
        }
2✔
84

85
        /// <returns>
86
        /// true if the game seems to be here and a version is found,
87
        /// false otherwise
88
        /// </returns>
89
        public bool Valid => game.GameInFolder(new DirectoryInfo(gameDir)) && Version() != null;
2✔
90

91
        /// <returns>
92
        /// true if the instance may be locked, false otherwise.
93
        /// Note that this is a tentative value; if it's true,
94
        /// we still need to try to acquire the lock to confirm it isn't stale.
95
        /// NOTE: Will throw NotGameDirKraken if the instance isn't valid!
96
        ///       Either be prepared to catch that exception, or check Valid first to avoid it.
97
        /// </returns>
98
        public bool IsMaybeLocked => RegistryManager.IsInstanceMaybeLocked(CkanDir());
×
99

100
        /// <summary>
101
        /// Create the CKAN directory and any supporting files.
102
        /// </summary>
103
        [MemberNotNull(nameof(playTime))]
104
        private void SetupCkanDirectories()
105
        {
2✔
106
            log.InfoFormat("Initialising {0}", CkanDir());
2✔
107

108
            // TxFileManager knows if we are in a transaction
109
            TxFileManager txFileMgr = new TxFileManager();
2✔
110

111
            if (!Directory.Exists(CkanDir()))
2✔
112
            {
2✔
113
                User.RaiseMessage(Properties.Resources.GameInstanceSettingUp);
2✔
114
                User.RaiseMessage(Properties.Resources.GameInstanceCreatingDir, CkanDir());
2✔
115
                txFileMgr.CreateDirectory(CkanDir());
2✔
116
            }
2✔
117

118
            playTime = TimeLog.Load(TimeLog.GetPath(CkanDir())) ?? new TimeLog();
2✔
119

120
            if (!Directory.Exists(InstallHistoryDir()))
2✔
121
            {
2✔
122
                User.RaiseMessage(Properties.Resources.GameInstanceCreatingDir, InstallHistoryDir());
2✔
123
                txFileMgr.CreateDirectory(InstallHistoryDir());
2✔
124
            }
2✔
125
            log.InfoFormat("Initialised {0}", CkanDir());
2✔
126
        }
2✔
127

128
        #endregion
129

130
        #region Settings
131

132
        public void SetCompatibleVersions(List<GameVersion> compatibleVersions)
133
        {
2✔
134
            _compatibleVersions = compatibleVersions.Distinct()
2✔
135
                                                    .OrderDescending()
136
                                                    .ToList();
137
            SaveCompatibleVersions();
2✔
138
        }
2✔
139

140
        private void SaveCompatibleVersions()
141
        {
2✔
142
            JsonConvert.SerializeObject(new CompatibleGameVersions()
2✔
143
                {
144
                    GameVersionWhenWritten = Version()?.ToString(),
145
                    Versions               = _compatibleVersions.Select(v => v.ToString())
2✔
146
                                                                .OfType<string>()
147
                                                                .ToList()
148
                })
149
                .WriteThroughTo(CompatibleGameVersionsFile());
150
            GameVersionWhenCompatibleVersionsWereStored = Version();
2✔
151
        }
2✔
152

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
            {
×
160
                _compatibleVersions = compatibleGameVersions.Versions
×
161
                                                            .Select(GameVersion.Parse)
162
                                                            .ToList();
163

164
                // Get version without throwing exceptions for null
165
                GameVersion.TryParse(compatibleGameVersions.GameVersionWhenWritten, out GameVersion? mainVer);
×
166
                GameVersionWhenCompatibleVersionsWereStored = mainVer;
×
167
            }
×
168
            else if (Version() is GameVersion gv)
2!
169
            {
2✔
170
                _compatibleVersions = game.DefaultCompatibleVersions(gv)
2✔
171
                                          .ToList();
172
            }
2✔
173
        }
2✔
174

175
        private string CompatibleGameVersionsFile()
176
            => Path.Combine(CkanDir(), game.CompatibleVersionsFile);
2✔
177

178
        public List<GameVersion> GetCompatibleVersions()
179
            => new List<GameVersion>(_compatibleVersions);
2✔
180

181
        public HashSet<string> GetSuppressedCompatWarningIdentifiers
182
            => SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile)
×
183
                                                 .Identifiers;
184

185
        public void AddSuppressedCompatWarningIdentifiers(HashSet<string> idents)
186
        {
×
187
            var scwi = SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile);
×
188
            scwi.Identifiers.UnionWith(idents);
×
189
            scwi.SaveTo(SuppressedCompatWarningIdentifiersFile);
×
190
        }
×
191

192
        private string SuppressedCompatWarningIdentifiersFile
193
            => Path.Combine(CkanDir(), "suppressed_compat_warning_identifiers.json");
×
194

195
        public string[] InstallFilters
196
        {
197
            get => (File.Exists(InstallFiltersFile)
2✔
198
                        ? JsonConvert.DeserializeObject<string[]>(File.ReadAllText(InstallFiltersFile))
199
                        : null)
200
                   ?? Array.Empty<string>();
201

202
            #pragma warning disable IDE0027
203
            set
204
            {
2✔
205
                JsonConvert.SerializeObject(value)
2✔
206
                           .WriteThroughTo(InstallFiltersFile);
207
            }
2✔
208
            #pragma warning restore IDE0027
209
        }
210

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

213
        public StabilityToleranceConfig StabilityToleranceConfig
214
            => stabilityToleranceConfig ??= new StabilityToleranceConfig(StabilityToleranceFile);
2✔
215

216
        private StabilityToleranceConfig? stabilityToleranceConfig;
217

218
        private string StabilityToleranceFile
219
            => Path.Combine(CkanDir(), "stability_tolerance.json");
2✔
220

221
        #endregion
222

223
        #region Game Directory Detection and Versioning
224

225
        /// <summary>
226
        /// Returns the path to our portable version of game if ckan.exe is in the same
227
        /// directory as the game, or if the game is in the current directory.
228
        /// Otherwise, returns null.
229
        /// </summary>
230
        public static string? PortableDir(IGame game)
231
            => new string?[]
2!
232
               {
233
                   Assembly.GetExecutingAssembly()?.Location,
234
                   Process.GetCurrentProcess()?.MainModule?.FileName,
235
               }
236
                   .OfType<string>()
237
                   .Select(Path.GetDirectoryName)
238
                   .OfType<string>()
239
                   .Prepend(Directory.GetCurrentDirectory())
240
                   .Select(path => new DirectoryInfo(path))
2✔
241
                   .FirstOrDefault(game.GameInFolder)
242
                   ?.FullName;
243

244
        /// <summary>
245
        /// Detects the version of a game in a given directory.
246
        /// </summary>
247
        private GameVersion? DetectVersion(string directory)
248
            => game.DetectVersion(new DirectoryInfo(directory));
2✔
249

250
        #endregion
251

252
        #region Things which would be better as Properties
253

254
        public string GameDir()
255
            => gameDir;
2✔
256

257
        public string CkanDir()
258
        {
2✔
259
            if (!Valid)
2!
UNCOV
260
            {
×
UNCOV
261
                log.Error("Could not find game version");
×
NEW
262
                throw new NotGameDirKraken(gameDir, Properties.Resources.GameInstanceVersionNotFound);
×
263
            }
264
            return CKANPathUtils.NormalizePath(
2✔
265
                Path.Combine(GameDir(), "CKAN"));
266
        }
2✔
267

268
        public string DownloadCacheDir()
269
            => CKANPathUtils.NormalizePath(Path.Combine(CkanDir(), "downloads"));
2✔
270

271
        public string InstallHistoryDir()
272
            => CKANPathUtils.NormalizePath(Path.Combine(CkanDir(), "history"));
2✔
273

274
        public IOrderedEnumerable<FileInfo> InstallHistoryFiles()
UNCOV
275
            => Directory.EnumerateFiles(InstallHistoryDir(), "*.ckan")
×
UNCOV
276
                        .Select(f => new FileInfo(f))
×
UNCOV
277
                        .OrderByDescending(fi => fi.CreationTime);
×
278

279
        public GameVersion? Version()
280
            => version ??= DetectVersion(GameDir());
2✔
281

282
        public GameVersionCriteria VersionCriteria()
283
            => new GameVersionCriteria(Version(), _compatibleVersions);
2✔
284

285
        #endregion
286

287
        /// <summary>
288
        /// Returns path relative to this instance's GameDir.
289
        /// </summary>
290
        public string ToRelativeGameDir(string path)
291
            => CKANPathUtils.ToRelative(path, GameDir());
2✔
292

293
        /// <summary>
294
        /// Given a path relative to this instance's GameDir, returns the
295
        /// absolute path on the system.
296
        /// </summary>
297
        public string ToAbsoluteGameDir(string path)
298
            => CKANPathUtils.ToAbsolute(path, GameDir());
2✔
299

300
        /// <summary>
301
        /// https://xkcd.com/208/
302
        /// This regex matches things like GameData/Foo/Foo.1.2.dll
303
        /// </summary>
304
        private static readonly Regex dllPattern = new Regex(
2✔
305
            @"
306
                ^(?:.*/)?             # Directories (ending with /)
307
                (?<identifier>[^.]+)  # Our DLL name, up until the first dot.
308
                .*\.dll$              # Everything else, ending in dll
309
            ",
310
            RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
311
        );
312

313
        /// <summary>
314
        /// Find the identifier associated with a manually installed DLL
315
        /// </summary>
316
        /// <param name="relPath">Path of the DLL relative to game root</param>
317
        /// <returns>
318
        /// Identifier if found otherwise null
319
        /// </returns>
320
        public string? DllPathToIdentifier(string relPath)
321
            // DLLs only live in the primary or alternate mod directories
322
            => game.AlternateModDirectoriesRelative
2!
323
                   .Prepend(game.PrimaryModDirectoryRelative)
324
                   .Any(p => relPath.StartsWith($"{p}/", Platform.PathComparison))
2✔
325
               && dllPattern.Match(relPath) is { Success: true } match
326
                   ? Identifier.Sanitize(match.Groups["identifier"].Value)
327
                   : null;
328

329
        /// <summary>
330
        /// Generate a sequence of files in the game folder that weren't installed by CKAN
331
        /// </summary>
332
        /// <param name="registry">A Registry object that knows which files CKAN installed in this folder</param>
333
        /// <returns>Relative file paths as strings</returns>
334
        public IEnumerable<string> UnmanagedFiles(Registry registry)
335
            => Directory.EnumerateFiles(gameDir, "*", SearchOption.AllDirectories)
×
336
                        .Select(CKANPathUtils.NormalizePath)
337
                        .Where(absPath => !absPath.StartsWith(CkanDir()))
×
338
                        .Select(ToRelativeGameDir)
339
                        .Where(relPath =>
340
                            !game.StockFolders.Any(f => relPath.StartsWith($"{f}/"))
×
341
                            && registry.FileOwner(relPath) == null);
342

343
        /// <summary>
344
        /// Check whether a given path contains any files or folders installed by CKAN
345
        /// </summary>
346
        /// <param name="registry">A Registry object that knows which files CKAN installed in this folder</param>
347
        /// <param name="absPath">Absolute path to a folder to check</param>
348
        /// <returns>true if any descendants of given path were installed by CKAN, false otherwise</returns>
349
        public bool HasManagedFiles(Registry registry, string absPath)
350
            => registry.FileOwner(ToRelativeGameDir(absPath)) != null
×
351
               || (Directory.Exists(absPath)
352
                   && Directory.EnumerateFileSystemEntries(absPath, "*", SearchOption.AllDirectories)
353
                               .Any(f => registry.FileOwner(ToRelativeGameDir(f)) != null));
×
354

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

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

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

399
        public override string ToString()
UNCOV
400
            => string.Format(Properties.Resources.GameInstanceToString, game.ShortName, gameDir);
×
401

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

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

408
        public override int GetHashCode()
UNCOV
409
            => gameDir.GetHashCode();
×
410
    }
411

412
}
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