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

KSP-CKAN / CKAN / 18959752962

31 Oct 2025 01:19AM UTC coverage: 85.329% (+3.5%) from 81.873%
18959752962

Pull #4454

github

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

2005 of 2167 branches covered (92.52%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

27 existing lines in 19 files now uncovered.

11971 of 14212 relevant lines covered (84.23%)

1.76 hits per line

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

72.09
/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()
17
            : this(SteamPaths.FirstOrDefault(p => !string.IsNullOrEmpty(p)
×
18
                                                  && Directory.Exists(p)
19
                                                  && File.Exists(LibraryFoldersConfigPath(p))))
20
        {
×
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
                                                   () => DeserializeAll<Dictionary<int, LibraryFolder>>(txtParser, 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)
UNCOV
67
            => Games.Where(g => gameDir.FullName.Equals(g.GameDir?.FullName, Platform.PathComparison))
×
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 => DeserializeAll<SteamGame>(acfParser, 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(() => DeserializeAll<Dictionary<int, NonSteamGame>>(vdfParser, 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
        private static T DeserializeAll<T>(KVSerializer serializer, Stream stream)
111
        {
2✔
112
            using (stream)
2✔
113
            {
2✔
114
                return serializer.Deserialize<T>(stream);
2✔
115
            }
116
        }
2✔
117

118
        /// <summary>
119
        /// Find the location where the current user's application data resides. Specific to macOS.
120
        /// </summary>
121
        /// <returns>
122
        ///     The application data folder, e.g. <code>/Users/USER/Library/Application Support</code>
123
        /// </returns>
124
        private static string GetMacOSApplicationDataFolder()
125
        {
×
126
            Debug.Assert(Platform.IsMac);
×
127

128
#if NET8_0_OR_GREATER
129
                // https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/8.0/getfolderpath-unix
130
                return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
131
#else
132
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
×
133
                "Library", "Application Support");
134
#endif
135
        }
×
136

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

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

163
        private static readonly ILog log = LogManager.GetLogger(typeof(SteamLibrary));
2✔
164
    }
165

166
    public class LibraryFolder
167
    {
168
        [KVProperty("path")] public string? Path { get; set; }
169
    }
170

171
    public abstract class GameBase
172
    {
173
        public abstract string? Name { get; set; }
174

175
        [KVIgnore] public          DirectoryInfo? GameDir   { get; set; }
176
        [KVIgnore] public abstract Uri            LaunchUrl { get;      }
177

178
        public abstract GameBase NormalizeDir(string appPath);
179
    }
180

181
    public class SteamGame : GameBase
182
    {
183
        [KVProperty("appid")]      public          ulong   AppId      { get; set; }
184
        [KVProperty("name")]       public override string? Name       { get; set; }
185
        [KVProperty("installdir")] public          string? InstallDir { get; set; }
186

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

190
        public override GameBase NormalizeDir(string commonPath)
191
        {
2✔
192
            if (InstallDir != null)
2✔
193
            {
2✔
194
                GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(Path.Combine(commonPath, InstallDir)));
2✔
195
            }
2✔
196
            return this;
2✔
197
        }
2✔
198
    }
199

200
    public class NonSteamGame : GameBase
201
    {
202
        [KVProperty("appid")]
203
        public          int     AppId    { get; set; }
204
        [KVProperty("AppName")]
205
        public override string? Name     { get; set; }
206
        public          string? Exe      { get; set; }
207
        public          string? StartDir { get; set; }
208

209
        [KVIgnore]
210
        private ulong UrlId => (unchecked((ulong)AppId) << 32) | 0x02000000;
2✔
211

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

215
        public override GameBase NormalizeDir(string appPath)
216
        {
2✔
217
            GameDir = StartDir == null ? null
2✔
218
                                       : Utilities.DefaultIfThrows(() =>
219
                                           new DirectoryInfo(CKANPathUtils.NormalizePath(StartDir.Trim('"'))));
2✔
220
            return this;
2✔
221
        }
2✔
222
    }
223
}
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