SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
steampp.cpp
Go to the documentation of this file.
1
5#include <steampp/steampp.h>
6
7#include <algorithm>
8#include <filesystem>
9#include <format>
10#include <ranges>
11#include <unordered_set>
12
13#ifdef _WIN32
14#include <memory>
15#include <Windows.h>
16#else
17#include <cstdlib>
18#endif
19
20#include <kvpp/kvpp.h>
21#include <sourcepp/FS.h>
22
23using namespace kvpp;
24using namespace sourcepp;
25using namespace steampp;
26
27namespace {
28
29bool isAppUsingGoldSrcEnginePredicate(std::string_view installDir) {
30 std::filesystem::directory_iterator dirIterator{installDir, std::filesystem::directory_options::skip_permission_denied};
31 std::error_code ec;
32 return std::any_of(std::filesystem::begin(dirIterator), std::filesystem::end(dirIterator), [&ec](const auto& entry){
33 return entry.is_directory(ec) && std::filesystem::exists(entry.path() / "liblist.gam", ec);
34 });
35}
36
37bool isAppUsingSourceEnginePredicate(std::string_view installDir) {
38 std::filesystem::directory_iterator dirIterator{installDir, std::filesystem::directory_options::skip_permission_denied};
39 std::error_code ec;
40 return std::any_of(std::filesystem::begin(dirIterator), std::filesystem::end(dirIterator), [&ec](const auto& entry){
41 return entry.is_directory(ec) && std::filesystem::exists(entry.path() / "gameinfo.txt", ec);
42 });
43}
44
45bool isAppUsingSource2EnginePredicate(std::string_view installDir) {
46 std::filesystem::directory_iterator dirIterator{installDir, std::filesystem::directory_options::skip_permission_denied};
47 std::error_code ec;
48 return std::any_of(std::filesystem::begin(dirIterator), std::filesystem::end(dirIterator), [&ec](const auto& entry) {
49 if (!entry.is_directory(ec)) {
50 return false;
51 }
52 if (std::filesystem::exists(entry.path() / "gameinfo.gi", ec)) {
53 return true;
54 }
55 std::filesystem::directory_iterator subDirIterator{entry.path(), std::filesystem::directory_options::skip_permission_denied};
56 return std::any_of(std::filesystem::begin(subDirIterator), std::filesystem::end(subDirIterator), [&ec](const auto& entry_) {
57 return entry_.is_directory(ec) && std::filesystem::exists(entry_.path() / "gameinfo.gi", ec);
58 });
59 });
60}
61
62// Note: this can't be a template because gcc threw a fit. No idea why
63std::unordered_set<AppID> getAppsKnownToUseEngine(bool(*p)(std::string_view)) {
64 if (p == &::isAppUsingGoldSrcEnginePredicate) {
65 return {
67 };
68 }
69 if (p == &::isAppUsingSourceEnginePredicate) {
70 return {
72 };
73 }
74 if (p == &::isAppUsingSource2EnginePredicate) {
75 return {
77 };
78 }
79 return {};
80}
81
82template<bool(*P)(std::string_view)>
83bool isAppUsingEngine(const Steam* steam, AppID appID) {
84 static std::unordered_set<AppID> knownIs = ::getAppsKnownToUseEngine(P);
85 if (knownIs.contains(appID)) {
86 return true;
87 }
88
89 static std::unordered_set<AppID> knownIsNot;
90 if (knownIsNot.contains(appID)) {
91 return false;
92 }
93
94 if (!steam->isAppInstalled(appID)) {
95 return false;
96 }
97
98 const auto installDir = steam->getAppInstallDir(appID);
99 if (std::error_code ec; !std::filesystem::exists(installDir, ec)) [[unlikely]] {
100 return false;
101 }
102
103 if (P(installDir)) {
104 knownIs.emplace(appID);
105 return true;
106 }
107 knownIsNot.emplace(appID);
108 return false;
109}
110
111[[nodiscard]] std::string getAppArtPath(const KV1Binary& assetCache, AppID appID, std::string_view steamInstallDir, std::string_view id) {
112 if (
113 assetCache.getChildCount() != 1 ||
114 assetCache[0].getChildCount() != 3 ||
115 !assetCache[0].hasChild("cache_version") ||
116 static_cast<KV1BinaryValueType>(assetCache[0]["cache_version"].getValue().index()) != KV1BinaryValueType::INT32 ||
117 *assetCache[0]["cache_version"].getValue<int32_t>() != 2
118 ) {
119 return "";
120 }
121 const auto idStr = std::format("{}", appID);
122 const auto& cache = assetCache[0][2][idStr];
123 if (cache.isInvalid() || !cache.hasChild(id)) {
124 return "";
125 }
126 auto path = (std::filesystem::path{steamInstallDir} / "appcache" / "librarycache" / idStr / *cache[id].getValue<std::string>()).string();
127 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
128 return "";
129 }
130 return path;
131}
132
133} // namespace
134
136 std::filesystem::path steamLocation;
137 std::error_code ec;
138
139#ifdef _WIN32
140 {
141 // 16383 being the maximum length of a path
142 static constexpr DWORD STEAM_LOCATION_MAX_SIZE = 16383;
143 std::unique_ptr<char[]> steamLocationData{new char[STEAM_LOCATION_MAX_SIZE]};
144
145 HKEY steam;
146 if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Valve\Steam)", 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &steam) != ERROR_SUCCESS) {
147 return;
148 }
149
150 DWORD steamLocationSize = STEAM_LOCATION_MAX_SIZE;
151 if (RegQueryValueExA(steam, "InstallPath", nullptr, nullptr, reinterpret_cast<LPBYTE>(steamLocationData.get()), &steamLocationSize) != ERROR_SUCCESS) {
152 return;
153 }
154
155 RegCloseKey(steam);
156 steamLocation = steamLocationSize > 0 ? std::string(steamLocationData.get(), steamLocationSize - 1) : "";
157 }
158#else
159 {
160 std::filesystem::path home{std::getenv("HOME")};
161#ifdef __APPLE__
162 steamLocation = home / "Library" / "Application Support" / "Steam";
163#else
164 // Snap install takes priority, the .steam symlink may exist simultaneously with Snap installs
165 steamLocation = home / "snap" / "steam" / "common" / ".steam" / "steam";
166
167 if (!std::filesystem::exists(steamLocation, ec)) {
168 // Use the regular install path
169 steamLocation = home / ".steam" / "steam";
170 }
171#endif
172 }
173
174 if (!std::filesystem::exists(steamLocation, ec)) {
175 std::string location;
176 std::filesystem::path d{"cwd/steamclient64.dll"};
177 for (const auto& entry : std::filesystem::directory_iterator{"/proc/"}) {
178 if (std::filesystem::exists(entry / d, ec)) {
179 ec.clear();
180 const auto s = std::filesystem::read_symlink(entry.path() / "cwd", ec);
181 if (ec) {
182 continue;
183 }
184 location = s.string();
185 break;
186 }
187 }
188 if (location.empty()) {
189 return;
190 }
191 steamLocation = location;
192 }
193#endif
194
195 if (!std::filesystem::exists(steamLocation.string(), ec)) {
196 return;
197 }
198 this->steamInstallDir = steamLocation.string();
199
200 auto libraryFoldersFilePath = steamLocation / "config" / "libraryfolders.vdf";
201 if (!std::filesystem::exists(libraryFoldersFilePath, ec)) {
202 libraryFoldersFilePath = steamLocation / "steamapps" / "libraryfolders.vdf";
203 if (!std::filesystem::exists(libraryFoldersFilePath, ec)) {
204 return;
205 }
206 }
207
208 KV1 libraryFolders{fs::readFileText(libraryFoldersFilePath.string())};
209
210 const auto& libraryFoldersValue = libraryFolders["libraryfolders"];
211 if (libraryFoldersValue.isInvalid()) {
212 return;
213 }
214
215 for (uint64_t i = 0; i < libraryFoldersValue.getChildCount(); i++) {
216 const auto& folder = libraryFoldersValue[i];
217
218 auto folderName = folder.getKey();
219 if (folderName == "TimeNextStatsReport" || folderName == "ContentStatsID") {
220 continue;
221 }
222
223 const auto& folderPath = folder["path"];
224 if (folderPath.isInvalid()) {
225 continue;
226 }
227
228 std::filesystem::path libraryFolderPath{folderPath.getValue()};
229 {
230 std::string libraryFolderPathProcessedEscapes;
231 bool hitBackslash = false;
232 for (char c : libraryFolderPath.string()) {
233 if (hitBackslash || c != '\\') {
234 libraryFolderPathProcessedEscapes += c;
235 hitBackslash = false;
236 } else {
237 hitBackslash = true;
238 }
239 }
240 libraryFolderPath = libraryFolderPathProcessedEscapes;
241 }
242 libraryFolderPath /= "steamapps";
243
244 if (!std::filesystem::exists(libraryFolderPath, ec)) {
245 continue;
246 }
247 this->libraryDirs.push_back(libraryFolderPath.string());
248
249 for (const auto& entry : std::filesystem::directory_iterator{libraryFolderPath, std::filesystem::directory_options::skip_permission_denied}) {
250 auto entryName = entry.path().filename().string();
251 if (!entryName.starts_with("appmanifest_") || !entryName.ends_with(".acf")) {
252 continue;
253 }
254
255 KV1 appManifest(fs::readFileText(entry.path().string()));
256
257 const auto& appState = appManifest["AppState"];
258 if (appState.isInvalid()) {
259 continue;
260 }
261
262 const auto& appName = appState["name"];
263 if (appName.isInvalid()) {
264 continue;
265 }
266 const auto& appInstallDir = appState["installdir"];
267 if (appInstallDir.isInvalid()) {
268 continue;
269 }
270 const auto& appID = appState["appid"];
271 if (appID.isInvalid()) {
272 continue;
273 }
274
275 this->gameDetails[std::stoi(std::string{appID.getValue()})] = GameInfo{
276 .name = std::string{appName.getValue()},
277 .installDir = std::string{appInstallDir.getValue()},
278 .libraryInstallDirsIndex = this->libraryDirs.size() - 1,
279 };
280 }
281 }
282
283 const auto assetCacheFilePath = steamLocation / "appcache" / "librarycache" / "assetcache.vdf";
284 if (std::filesystem::exists(assetCacheFilePath, ec)) {
285 this->assetCache = KV1Binary{fs::readFileBuffer(assetCacheFilePath.string())};
286 }
287}
288
289std::string_view Steam::getInstallDir() const {
290 return this->steamInstallDir;
291}
292
293const std::vector<std::string>& Steam::getLibraryDirs() const {
294 return this->libraryDirs;
295}
296
297std::string Steam::getSourceModDir() const {
298 return (std::filesystem::path{this->steamInstallDir} / "steamapps" / "sourcemods").string();
299}
300
301std::vector<AppID> Steam::getInstalledApps() const {
302 auto keys = std::views::keys(this->gameDetails);
303 return {keys.begin(), keys.end()};
304}
305
306bool Steam::isAppInstalled(AppID appID) const {
307 return this->gameDetails.contains(appID);
308}
309
310std::string_view Steam::getAppName(AppID appID) const {
311 if (!this->gameDetails.contains(appID)) {
312 return "";
313 }
314 return this->gameDetails.at(appID).name;
315}
316
317std::string Steam::getAppInstallDir(AppID appID) const {
318 if (!this->gameDetails.contains(appID)) {
319 return "";
320 }
321 return (std::filesystem::path{this->libraryDirs[this->gameDetails.at(appID).libraryInstallDirsIndex]} / "common" / this->gameDetails.at(appID).installDir).string();
322}
323
324std::string Steam::getAppIconPath(AppID appID) const {
325 if (!this->gameDetails.contains(appID)) {
326 return "";
327 }
328 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "4f"); !cachedPath.empty()) {
329 return cachedPath;
330 }
331 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "icon.jpg").string();
332 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
333 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_icon.jpg")).string();
334 if (!std::filesystem::exists(path, ec)) {
335 return "";
336 }
337 }
338 return path;
339}
340
341std::string Steam::getAppLogoPath(AppID appID) const {
342 if (!this->gameDetails.contains(appID)) {
343 return "";
344 }
345 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "2f"); !cachedPath.empty()) {
346 return cachedPath;
347 }
348 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "logo.png").string();
349 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
350 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_logo.png")).string();
351 if (!std::filesystem::exists(path, ec)) {
352 return "";
353 }
354 }
355 return path;
356}
357
358std::string Steam::getAppHeroPath(AppID appID) const {
359 if (!this->gameDetails.contains(appID)) {
360 return "";
361 }
362 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "1f"); !cachedPath.empty()) {
363 return cachedPath;
364 }
365 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "library_hero.jpg").string();
366 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
367 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_library_hero.jpg")).string();
368 if (!std::filesystem::exists(path, ec)) {
369 return "";
370 }
371 }
372 return path;
373}
374
375std::string Steam::getAppBoxArtPath(AppID appID) const {
376 if (!this->gameDetails.contains(appID)) {
377 return "";
378 }
379 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "0f"); !cachedPath.empty()) {
380 return cachedPath;
381 }
382 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "library_600x900.jpg").string();
383 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
384 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_library_600x900.jpg")).string();
385 if (!std::filesystem::exists(path, ec)) {
386 return "";
387 }
388 }
389 return path;
390}
391
392std::string Steam::getAppStoreArtPath(AppID appID) const {
393 if (!this->gameDetails.contains(appID)) {
394 return "";
395 }
396 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "3f"); !cachedPath.empty()) {
397 return cachedPath;
398 }
399 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "header.jpg").string();
400 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
401 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_header.jpg")).string();
402 if (!std::filesystem::exists(path, ec)) {
403 return "";
404 }
405 }
406 return path;
407}
408
410 return ::isAppUsingEngine<::isAppUsingGoldSrcEnginePredicate>(this, appID);
411}
412
414 return ::isAppUsingEngine<::isAppUsingSourceEnginePredicate>(this, appID);
415}
416
418 return ::isAppUsingEngine<::isAppUsingSource2EnginePredicate>(this, appID);
419}
420
421Steam::operator bool() const {
422 return !this->gameDetails.empty();
423}
uint64_t getChildCount() const
Get the number of child elements.
Definition: KV1Binary.cpp:37
Definition: KV1.h:221
std::vector< AppID > getInstalledApps() const
Definition: steampp.cpp:301
bool isAppUsingSourceEngine(AppID appID) const
Definition: steampp.cpp:413
bool isAppUsingGoldSrcEngine(AppID appID) const
Definition: steampp.cpp:409
std::string getAppBoxArtPath(AppID appID) const
Definition: steampp.cpp:375
std::string getSourceModDir() const
Definition: steampp.cpp:297
const std::vector< std::string > & getLibraryDirs() const
Definition: steampp.cpp:293
bool isAppUsingSource2Engine(AppID appID) const
Definition: steampp.cpp:417
std::string getAppStoreArtPath(AppID appID) const
Definition: steampp.cpp:392
std::string_view getAppName(AppID appID) const
Definition: steampp.cpp:310
bool isAppInstalled(AppID appID) const
Definition: steampp.cpp:306
std::string getAppLogoPath(AppID appID) const
Definition: steampp.cpp:341
std::string getAppHeroPath(AppID appID) const
Definition: steampp.cpp:358
std::string getAppInstallDir(AppID appID) const
Definition: steampp.cpp:317
std::string_view getInstallDir() const
Definition: steampp.cpp:289
std::string getAppIconPath(AppID appID) const
Definition: steampp.cpp:324
Definition: KV1.h:13
KV1BinaryValueType
Definition: KV1Binary.h:16
std::vector< std::byte > readFileBuffer(const std::string &filepath, std::size_t startOffset=0)
Definition: FS.cpp:9
std::string readFileText(const std::string &filepath, std::size_t startOffset=0)
Definition: FS.cpp:18
Definition: LZMA.h:11
Based on SteamAppPathProvider.
Definition: steampp.h:16
uint32_t AppID
Definition: steampp.h:18