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

KSP-CKAN / CKAN / 15714778159

17 Jun 2025 06:03PM UTC coverage: 28.326% (+1.2%) from 27.151%
15714778159

Pull #4396

github

web-flow
Merge 61a285d24 into a01cebc8d
Pull Request #4396: More tests for Core

3880 of 12089 branches covered (32.1%)

Branch coverage included in aggregate %.

12 of 42 new or added lines in 12 files covered. (28.57%)

5 existing lines in 2 files now uncovered.

8370 of 31158 relevant lines covered (26.86%)

0.55 hits per line

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

66.32
/Core/IO/SteamLibrary.cs
1
using System;
2
using System.IO;
3
using System.Linq;
4
using System.Collections.Generic;
5
using System.Diagnostics;
6

7
using log4net;
8
using ValveKeyValue;
9

10
using CKAN.Extensions;
11

12
namespace CKAN.IO
13
{
14
    public class SteamLibrary
15
    {
16
        public SteamLibrary()
UNCOV
17
            : this(SteamPaths.FirstOrDefault(p => !string.IsNullOrEmpty(p)
×
18
                                                  && Directory.Exists(p)
19
                                                  && File.Exists(LibraryFoldersConfigPath(p))))
UNCOV
20
        {
×
UNCOV
21
        }
×
22

23
        public SteamLibrary(string? libraryPath)
2✔
24
        {
2✔
25
            if (libraryPath != null
2✔
26
                && LibraryFoldersConfigPath(libraryPath) is string libFoldersConfigPath
27
                && OpenRead(libFoldersConfigPath) is FileStream stream)
28
            {
2✔
29
                log.InfoFormat("Found Steam at {0}", libraryPath);
2✔
30
                var txtParser     = KVSerializer.Create(KVSerializationFormat.KeyValues1Text);
2✔
31
                var appPaths      = (Utilities.DefaultIfThrows(
2✔
32
                                                   () => txtParser.Deserialize<Dictionary<int, LibraryFolder>>(stream),
2✔
33
                                                   exc =>
34
                                                   {
×
35
                                                       log.Warn($"Failed to parse {libFoldersConfigPath}", exc);
×
36
                                                       return null;
×
37
                                                   })
×
38
                                              ?.Values
39
                                               .Select(lf => lf.Path is string libPath
2!
40
                                                             ? appRelPaths.Select(p => Path.Combine(libPath, p))
2✔
41
                                                                          .FirstOrDefault(Directory.Exists)
42
                                                             : null)
43
                                               .OfType<string>()
44
                                              ?? Enumerable.Empty<string>())
45
                                    .ToArray();
46
                var steamGames    = appPaths.SelectMany(p => LibraryPathGames(txtParser, p));
2✔
47
                var binParser     = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary);
2✔
48
                var nonSteamGames = Directory.EnumerateDirectories(Path.Combine(libraryPath, "userdata"))
2✔
49
                                             .Select(dirName => Path.Combine(dirName, "config"))
2✔
50
                                             .Where(Directory.Exists)
51
                                             .Select(dirName => Path.Combine(dirName, "shortcuts.vdf"))
2✔
52
                                             .Where(File.Exists)
53
                                             .SelectMany(p => ShortcutsFileGames(binParser, p));
2✔
54
                Games = steamGames.Concat(nonSteamGames)
2✔
55
                                  .ToArray();
56
                log.DebugFormat("Games: {0}",
2✔
57
                                string.Join(", ", Games.Select(g => $"{g.LaunchUrl} ({g.GameDir})")));
2✔
58
            }
2✔
59
            else
60
            {
2✔
61
                log.Info("Steam not found");
2✔
62
                Games = Array.Empty<NonSteamGame>();
2✔
63
            }
2✔
64
        }
2✔
65

66
        public IEnumerable<Uri> GameAppURLs(DirectoryInfo gameDir)
67
            => Games.Where(g => gameDir.FullName.Equals(g.GameDir?.FullName, Platform.PathComparison))
1✔
68
                    .Select(g => g.LaunchUrl);
×
69

70
        public readonly GameBase[] Games;
71

72
        public static bool IsSteamCmdLine(string command)
73
            => command.StartsWith("steam://", StringComparison.InvariantCultureIgnoreCase);
2✔
74

75
        private static string LibraryFoldersConfigPath(string libraryPath)
76
            => Path.Combine(libraryPath, "config", "libraryfolders.vdf");
2✔
77

78
        private static FileStream? OpenRead(string path)
79
            => Utilities.DefaultIfThrows(() => File.OpenRead(path),
2✔
80
                                         exc =>
81
                                         {
×
82
                                             log.Warn($"Failed to open {path}", exc);
×
83
                                             return null;
×
84
                                         });
×
85

86
        private static IEnumerable<GameBase> LibraryPathGames(KVSerializer acfParser,
87
                                                              string       appPath)
88
            => Directory.EnumerateFiles(appPath, "*.acf")
2✔
89
                        .SelectWithCatch(acfFile => acfParser.Deserialize<SteamGame>(File.OpenRead(acfFile))
2✔
90
                                                             .NormalizeDir(Path.Combine(appPath, "common")),
91
                                         (acfFile, exc) =>
92
                                         {
2✔
93
                                             log.Warn($"Failed to parse {acfFile}:", exc);
2✔
94
                                             return default;
2✔
95
                                         })
2✔
96
                        .OfType<GameBase>();
97

98
        private static IEnumerable<GameBase> ShortcutsFileGames(KVSerializer vdfParser,
99
                                                                string       path)
100
            => Utilities.DefaultIfThrows(() => vdfParser.Deserialize<Dictionary<int, NonSteamGame>>(File.OpenRead(path)),
2!
101
                                         exc =>
102
                                         {
×
103
                                             log.Warn($"Failed to parse {path}", exc);
×
104
                                             return null;
×
105
                                         })
×
106
                        ?.Values
107
                         .Select(nsg => nsg.NormalizeDir(path))
2✔
108
                        ?? Enumerable.Empty<NonSteamGame>();
109

110
        /// <summary>
111
        /// Find the location where the current user's application data resides. Specific to macOS.
112
        /// </summary>
113
        /// <returns>
114
        ///     The application data folder, e.g. <code>/Users/USER/Library/Application Support</code>
115
        /// </returns>
116
        private static string GetMacOSApplicationDataFolder()
117
        {
×
118
            Debug.Assert(Platform.IsMac);
×
119

120
#if NET8_0_OR_GREATER
121
                // https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/8.0/getfolderpath-unix
122
                return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
123
#else
124
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
×
125
                "Library", "Application Support");
126
#endif
127
        }
×
128

129
        private const  string   registryKey   = @"HKEY_CURRENT_USER\Software\Valve\Steam";
130
        private const  string   registryValue = @"SteamPath";
131
        private static string[] SteamPaths
UNCOV
132
            => Platform.IsWindows
×
133
               // First check the registry
134
               && Microsoft.Win32.Registry.GetValue(registryKey, registryValue, "") is string val
135
               && !string.IsNullOrEmpty(val)
136
            ? new string[]
137
            {
138
                val,
139
            }
140
            : Platform.IsUnix ? new string[]
141
            {
142
                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
143
                             "Steam"),
144
                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
145
                             ".steam", "steam"),
146
            }
147
            : Platform.IsMac ? new string[]
148
            {
149
                Path.Combine(GetMacOSApplicationDataFolder(), "Steam"),
150
            }
151
            : Array.Empty<string>();
152

153
        private static readonly string[] appRelPaths = new string[] { "SteamApps", "steamapps" };
2✔
154

155
        private static readonly ILog log = LogManager.GetLogger(typeof(SteamLibrary));
2✔
156
    }
157

158
    public class LibraryFolder
159
    {
160
        [KVProperty("path")] public string? Path { get; set; }
161
    }
162

163
    public abstract class GameBase
164
    {
165
        public abstract string? Name { get; set; }
166

167
        [KVIgnore] public          DirectoryInfo? GameDir   { get; set; }
168
        [KVIgnore] public abstract Uri            LaunchUrl { get;      }
169

170
        public abstract GameBase NormalizeDir(string appPath);
171
    }
172

173
    public class SteamGame : GameBase
174
    {
175
        [KVProperty("appid")]      public          ulong   AppId      { get; set; }
176
        [KVProperty("name")]       public override string? Name       { get; set; }
177
        [KVProperty("installdir")] public          string? InstallDir { get; set; }
178

179
        [KVIgnore]
180
        public override Uri LaunchUrl => new Uri($"steam://rungameid/{AppId}");
2✔
181

182
        public override GameBase NormalizeDir(string commonPath)
183
        {
2✔
184
            if (InstallDir != null)
2!
185
            {
2✔
186
                GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(Path.Combine(commonPath, InstallDir)));
2✔
187
            }
2✔
188
            return this;
2✔
189
        }
2✔
190
    }
191

192
    public class NonSteamGame : GameBase
193
    {
194
        [KVProperty("appid")]
195
        public          int     AppId    { get; set; }
196
        [KVProperty("AppName")]
197
        public override string? Name     { get; set; }
198
        public          string? Exe      { get; set; }
199
        public          string? StartDir { get; set; }
200

201
        [KVIgnore]
202
        private ulong UrlId => (unchecked((ulong)AppId) << 32) | 0x02000000;
2✔
203

204
        [KVIgnore]
205
        public override Uri LaunchUrl => new Uri($"steam://rungameid/{UrlId}");
2✔
206

207
        public override GameBase NormalizeDir(string appPath)
208
        {
2✔
209
            GameDir = StartDir == null ? null
2✔
210
                                       : Utilities.DefaultIfThrows(() =>
211
                                           new DirectoryInfo(CKANPathUtils.NormalizePath(StartDir.Trim('"'))));
2✔
212
            return this;
2✔
213
        }
2✔
214
    }
215
}
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