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{"~"};
168 if (const auto* homeEnv = std::getenv("HOME")) {
169 HOME = homeEnv;
170 }
171#ifdef __APPLE__
172 steamLocation = HOME / "Library" / "Application Support" / "Steam";
173#else
174 std::filesystem::path XDG_DATA_HOME{HOME / ".local" / "share"};
175 if (const auto* xdgDataHomeEnv = std::getenv("XDG_DATA_HOME")) {
176 XDG_DATA_HOME = xdgDataHomeEnv;
177 }
178
179 const std::array locations{
180 HOME / "snap" / "steam" / "common" / ".local" / "share" / "Steam", // snap install
181 HOME / "snap" / "steam" / "common" / ".steam" / "steam", // snap symlink
182 HOME / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam", // flatpak install
183 HOME / ".var" / "app" / "com.valvesoftware.Steam" / ".steam" / "steam", // flatpak symlink
184 XDG_DATA_HOME / "Steam", // expected install (XDG_DATA_HOME)
185 HOME / ".local" / "share" / "Steam", // expected install (HOME)
186 HOME / ".steam" / "steam", // expected symlink
187 };
188
189 for (const auto& location : locations) {
190 if (std::filesystem::exists(location, ec)) {
191 steamLocation = location;
192 break;
193 }
194 }
195 if (steamLocation.empty()) {
196 // Find where the Steam process is running from
197 std::filesystem::path location;
198 std::filesystem::path d{"cwd/steamclient64.dll"};
199 for (const auto& entry : std::filesystem::directory_iterator{"/proc/"}) {
200 if (std::filesystem::exists(entry / d, ec)) {
201 ec.clear();
202 location = std::filesystem::read_symlink(entry.path() / "cwd", ec);
203 if (ec) {
204 continue;
205 }
206 break;
207 }
208 }
209 if (location.empty()) {
210 return;
211 }
212 steamLocation = location;
213 }
214#endif
215 }
216#endif
217
218 if (steamLocation.empty()) {
219 return;
220 }
221 this->steamInstallDir = steamLocation;
222
223 auto libraryFoldersFilePath = steamLocation / "config" / "libraryfolders.vdf";
224 if (!std::filesystem::exists(libraryFoldersFilePath, ec)) {
225 libraryFoldersFilePath = steamLocation / "steamapps" / "libraryfolders.vdf";
226 if (!std::filesystem::exists(libraryFoldersFilePath, ec)) {
227 return;
228 }
229 }
230
231 KV1 libraryFolders{fs::readFileText(libraryFoldersFilePath), true};
232
233 const auto& libraryFoldersValue = libraryFolders["libraryfolders"];
234 if (libraryFoldersValue.isInvalid()) {
235 return;
236 }
237
238 for (uint64_t i = 0; i < libraryFoldersValue.getChildCount(); i++) {
239 const auto& folder = libraryFoldersValue[i];
240
241 auto folderName = folder.getKey();
242 if (folderName == "TimeNextStatsReport" || folderName == "ContentStatsID") {
243 continue;
244 }
245
246 const auto& folderPath = folder["path"];
247 if (folderPath.isInvalid()) {
248 continue;
249 }
250
251 std::filesystem::path libraryFolderPath{folderPath.getValue()};
252 libraryFolderPath /= "steamapps";
253
254 if (!std::filesystem::exists(libraryFolderPath, ec)) {
255 continue;
256 }
257 this->libraryDirs.push_back(libraryFolderPath);
258
259 for (const auto& entry : std::filesystem::directory_iterator{libraryFolderPath, std::filesystem::directory_options::skip_permission_denied}) {
260 auto entryName = entry.path().filename().string();
261 if (!entryName.starts_with("appmanifest_") || !entryName.ends_with(".acf")) {
262 continue;
263 }
264
265 KV1 appManifest(fs::readFileText(entry.path()));
266
267 const auto& appState = appManifest["AppState"];
268 if (appState.isInvalid()) {
269 continue;
270 }
271
272 const auto& appName = appState["name"];
273 if (appName.isInvalid()) {
274 continue;
275 }
276 const auto& appInstallDir = appState["installdir"];
277 if (appInstallDir.isInvalid()) {
278 continue;
279 }
280 const auto& appID = appState["appid"];
281 if (appID.isInvalid()) {
282 continue;
283 }
284
285 this->gameDetails[std::stoi(std::string{appID.getValue()})] = GameInfo{
286 .name = std::string{appName.getValue()},
287 .installDir = std::string{appInstallDir.getValue()},
288 .libraryInstallDirsIndex = this->libraryDirs.size() - 1,
289 };
290 }
291 }
292
293 const auto assetCacheFilePath = steamLocation / "appcache" / "librarycache" / "assetcache.vdf";
294 if (std::filesystem::exists(assetCacheFilePath, ec)) {
295 this->assetCache = KV1Binary{fs::readFileBuffer(assetCacheFilePath)};
296 }
297}
298
299const std::filesystem::path& Steam::getInstallDir() const {
300 return this->steamInstallDir;
301}
302
303std::span<const std::filesystem::path> Steam::getLibraryDirs() const {
304 return this->libraryDirs;
305}
306
307std::filesystem::path Steam::getSourceModDir() const {
308 return this->steamInstallDir / "steamapps" / "sourcemods";
309}
310
311std::vector<AppID> Steam::getInstalledApps() const {
312 auto keys = std::views::keys(this->gameDetails);
313 return {keys.begin(), keys.end()};
314}
315
316bool Steam::isAppInstalled(AppID appID) const {
317 return this->gameDetails.contains(appID);
318}
319
320std::string_view Steam::getAppName(AppID appID) const {
321 if (!this->gameDetails.contains(appID)) {
322 return "";
323 }
324 return this->gameDetails.at(appID).name;
325}
326
327std::filesystem::path Steam::getAppInstallDir(AppID appID) const {
328 if (!this->gameDetails.contains(appID)) {
329 return "";
330 }
331 return this->libraryDirs[this->gameDetails.at(appID).libraryInstallDirsIndex] / "common" / this->gameDetails.at(appID).installDir;
332}
333
334std::filesystem::path Steam::getAppIconPath(AppID appID) const {
335 if (!this->gameDetails.contains(appID)) {
336 return "";
337 }
338 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "4f"); !cachedPath.empty()) {
339 return cachedPath;
340 }
341 auto path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}", appID) / "icon.jpg";
342 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
343 path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}_icon.jpg", appID);
344 if (!std::filesystem::exists(path, ec)) {
345 return "";
346 }
347 }
348 return path;
349}
350
351std::filesystem::path Steam::getAppLogoPath(AppID appID) const {
352 if (!this->gameDetails.contains(appID)) {
353 return "";
354 }
355 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "2f"); !cachedPath.empty()) {
356 return cachedPath;
357 }
358 auto path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}", appID) / "logo.png";
359 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
360 path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}_logo.png", appID);
361 if (!std::filesystem::exists(path, ec)) {
362 return "";
363 }
364 }
365 return path;
366}
367
368std::filesystem::path Steam::getAppHeroPath(AppID appID) const {
369 if (!this->gameDetails.contains(appID)) {
370 return "";
371 }
372 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "1f"); !cachedPath.empty()) {
373 return cachedPath;
374 }
375 auto path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}", appID) / "library_hero.jpg";
376 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
377 path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}_library_hero.jpg", appID);
378 if (!std::filesystem::exists(path, ec)) {
379 return "";
380 }
381 }
382 return path;
383}
384
385std::filesystem::path Steam::getAppBoxArtPath(AppID appID) const {
386 if (!this->gameDetails.contains(appID)) {
387 return "";
388 }
389 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "0f"); !cachedPath.empty()) {
390 return cachedPath;
391 }
392 auto path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}", appID) / "library_600x900.jpg";
393 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
394 path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}_library_600x900.jpg", appID);
395 if (!std::filesystem::exists(path, ec)) {
396 return "";
397 }
398 }
399 return path;
400}
401
402std::filesystem::path Steam::getAppStoreArtPath(AppID appID) const {
403 if (!this->gameDetails.contains(appID)) {
404 return "";
405 }
406 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "3f"); !cachedPath.empty()) {
407 return cachedPath;
408 }
409 auto path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}", appID) / "header.jpg";
410 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
411 path = this->steamInstallDir / "appcache" / "librarycache" / std::format("{}_header.jpg", appID);
412 if (!std::filesystem::exists(path, ec)) {
413 return "";
414 }
415 }
416 return path;
417}
418
420 return ::isAppUsingEngine<::isAppUsingGoldSrcEnginePredicate>(this, appID);
421}
422
424 return ::isAppUsingEngine<::isAppUsingSourceEnginePredicate>(this, appID);
425}
426
428 return ::isAppUsingEngine<::isAppUsingSource2EnginePredicate>(this, appID);
429}
430
431Steam::operator bool() const {
432 return !this->gameDetails.empty();
433}
const KV1BinaryValue & getValue() const
Get the value associated with the element.
Definition KV1Binary.cpp:25
std::vector< AppID > getInstalledApps() const
Definition steampp.cpp:311
bool isAppUsingSourceEngine(AppID appID) const
Definition steampp.cpp:423
bool isAppUsingGoldSrcEngine(AppID appID) const
Definition steampp.cpp:419
std::span< const std::filesystem::path > getLibraryDirs() const
Definition steampp.cpp:303
const std::filesystem::path & getInstallDir() const
Definition steampp.cpp:299
std::filesystem::path getAppLogoPath(AppID appID) const
Definition steampp.cpp:351
bool isAppUsingSource2Engine(AppID appID) const
Definition steampp.cpp:427
std::string_view getAppName(AppID appID) const
Definition steampp.cpp:320
std::filesystem::path getAppStoreArtPath(AppID appID) const
Definition steampp.cpp:402
bool isAppInstalled(AppID appID) const
Definition steampp.cpp:316
std::filesystem::path getAppHeroPath(AppID appID) const
Definition steampp.cpp:368
std::filesystem::path getAppBoxArtPath(AppID appID) const
Definition steampp.cpp:385
std::filesystem::path getAppInstallDir(AppID appID) const
Definition steampp.cpp:327
std::filesystem::path getSourceModDir() const
Definition steampp.cpp:307
std::filesystem::path getAppIconPath(AppID appID) const
Definition steampp.cpp:334
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:22
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