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