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 const 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 / "steamapps" / "libraryfolders.vdf";
201 if (!std::filesystem::exists(libraryFoldersFilePath, ec)) {
202 return;
203 }
204
205 KV1 libraryFolders{fs::readFileText(libraryFoldersFilePath.string())};
206
207 const auto& libraryFoldersValue = libraryFolders["libraryfolders"];
208 if (libraryFoldersValue.isInvalid()) {
209 return;
210 }
211
212 for (uint64_t i = 0; i < libraryFoldersValue.getChildCount(); i++) {
213 const auto& folder = libraryFoldersValue[i];
214
215 auto folderName = folder.getKey();
216 if (folderName == "TimeNextStatsReport" || folderName == "ContentStatsID") {
217 continue;
218 }
219
220 const auto& folderPath = folder["path"];
221 if (folderPath.isInvalid()) {
222 continue;
223 }
224
225 std::filesystem::path libraryFolderPath{folderPath.getValue()};
226 {
227 std::string libraryFolderPathProcessedEscapes;
228 bool hitBackslash = false;
229 for (char c : libraryFolderPath.string()) {
230 if (hitBackslash || c != '\\') {
231 libraryFolderPathProcessedEscapes += c;
232 hitBackslash = false;
233 } else {
234 hitBackslash = true;
235 }
236 }
237 libraryFolderPath = libraryFolderPathProcessedEscapes;
238 }
239 libraryFolderPath /= "steamapps";
240
241 if (!std::filesystem::exists(libraryFolderPath, ec)) {
242 continue;
243 }
244 this->libraryDirs.push_back(libraryFolderPath.string());
245
246 for (const auto& entry : std::filesystem::directory_iterator{libraryFolderPath, std::filesystem::directory_options::skip_permission_denied}) {
247 auto entryName = entry.path().filename().string();
248 if (!entryName.starts_with("appmanifest_") || !entryName.ends_with(".acf")) {
249 continue;
250 }
251
252 KV1 appManifest(fs::readFileText(entry.path().string()));
253
254 const auto& appState = appManifest["AppState"];
255 if (appState.isInvalid()) {
256 continue;
257 }
258
259 const auto& appName = appState["name"];
260 if (appName.isInvalid()) {
261 continue;
262 }
263 const auto& appInstallDir = appState["installdir"];
264 if (appInstallDir.isInvalid()) {
265 continue;
266 }
267 const auto& appID = appState["appid"];
268 if (appID.isInvalid()) {
269 continue;
270 }
271
272 this->gameDetails[std::stoi(std::string{appID.getValue()})] = GameInfo{
273 .name = std::string{appName.getValue()},
274 .installDir = std::string{appInstallDir.getValue()},
275 .libraryInstallDirsIndex = this->libraryDirs.size() - 1,
276 };
277 }
278 }
279
280 const auto assetCacheFilePath = steamLocation / "appcache" / "librarycache" / "assetcache.vdf";
281 if (std::filesystem::exists(assetCacheFilePath, ec)) {
282 this->assetCache = KV1Binary{fs::readFileBuffer(assetCacheFilePath.string())};
283 }
284}
285
286std::string_view Steam::getInstallDir() const {
287 return this->steamInstallDir;
288}
289
290const std::vector<std::string>& Steam::getLibraryDirs() const {
291 return this->libraryDirs;
292}
293
294std::string Steam::getSourceModDir() const {
295 return (std::filesystem::path{this->steamInstallDir} / "steamapps" / "sourcemods").string();
296}
297
298std::vector<AppID> Steam::getInstalledApps() const {
299 auto keys = std::views::keys(this->gameDetails);
300 return {keys.begin(), keys.end()};
301}
302
303bool Steam::isAppInstalled(AppID appID) const {
304 return this->gameDetails.contains(appID);
305}
306
307std::string_view Steam::getAppName(AppID appID) const {
308 if (!this->gameDetails.contains(appID)) {
309 return "";
310 }
311 return this->gameDetails.at(appID).name;
312}
313
314std::string Steam::getAppInstallDir(AppID appID) const {
315 if (!this->gameDetails.contains(appID)) {
316 return "";
317 }
318 return (std::filesystem::path{this->libraryDirs[this->gameDetails.at(appID).libraryInstallDirsIndex]} / "common" / this->gameDetails.at(appID).installDir).string();
319}
320
321std::string Steam::getAppIconPath(AppID appID) const {
322 if (!this->gameDetails.contains(appID)) {
323 return "";
324 }
325 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "4f"); !cachedPath.empty()) {
326 return cachedPath;
327 }
328 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "icon.jpg").string();
329 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
330 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_icon.jpg")).string();
331 if (!std::filesystem::exists(path, ec)) {
332 return "";
333 }
334 }
335 return path;
336}
337
338std::string Steam::getAppLogoPath(AppID appID) const {
339 if (!this->gameDetails.contains(appID)) {
340 return "";
341 }
342 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "2f"); !cachedPath.empty()) {
343 return cachedPath;
344 }
345 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "logo.png").string();
346 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
347 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_logo.png")).string();
348 if (!std::filesystem::exists(path, ec)) {
349 return "";
350 }
351 }
352 return path;
353}
354
355std::string Steam::getAppHeroPath(AppID appID) const {
356 if (!this->gameDetails.contains(appID)) {
357 return "";
358 }
359 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "1f"); !cachedPath.empty()) {
360 return cachedPath;
361 }
362 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "library_hero.jpg").string();
363 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
364 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_library_hero.jpg")).string();
365 if (!std::filesystem::exists(path, ec)) {
366 return "";
367 }
368 }
369 return path;
370}
371
372std::string Steam::getAppBoxArtPath(AppID appID) const {
373 if (!this->gameDetails.contains(appID)) {
374 return "";
375 }
376 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "0f"); !cachedPath.empty()) {
377 return cachedPath;
378 }
379 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "library_600x900.jpg").string();
380 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
381 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_library_600x900.jpg")).string();
382 if (!std::filesystem::exists(path, ec)) {
383 return "";
384 }
385 }
386 return path;
387}
388
389std::string Steam::getAppStoreArtPath(AppID appID) const {
390 if (!this->gameDetails.contains(appID)) {
391 return "";
392 }
393 if (const auto cachedPath = ::getAppArtPath(this->assetCache, appID, this->steamInstallDir, "3f"); !cachedPath.empty()) {
394 return cachedPath;
395 }
396 auto path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / std::to_string(appID) / "header.jpg").string();
397 if (std::error_code ec; !std::filesystem::exists(path, ec)) {
398 path = (std::filesystem::path{this->steamInstallDir} / "appcache" / "librarycache" / (std::to_string(appID) + "_header.jpg")).string();
399 if (!std::filesystem::exists(path, ec)) {
400 return "";
401 }
402 }
403 return path;
404}
405
407 return ::isAppUsingEngine<::isAppUsingGoldSrcEnginePredicate>(this, appID);
408}
409
411 return ::isAppUsingEngine<::isAppUsingSourceEnginePredicate>(this, appID);
412}
413
415 return ::isAppUsingEngine<::isAppUsingSource2EnginePredicate>(this, appID);
416}
417
418Steam::operator bool() const {
419 return !this->gameDetails.empty();
420}
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:298
bool isAppUsingSourceEngine(AppID appID) const
Definition: steampp.cpp:410
bool isAppUsingGoldSrcEngine(AppID appID) const
Definition: steampp.cpp:406
std::string getAppBoxArtPath(AppID appID) const
Definition: steampp.cpp:372
std::string getSourceModDir() const
Definition: steampp.cpp:294
const std::vector< std::string > & getLibraryDirs() const
Definition: steampp.cpp:290
bool isAppUsingSource2Engine(AppID appID) const
Definition: steampp.cpp:414
std::string getAppStoreArtPath(AppID appID) const
Definition: steampp.cpp:389
std::string_view getAppName(AppID appID) const
Definition: steampp.cpp:307
bool isAppInstalled(AppID appID) const
Definition: steampp.cpp:303
std::string getAppLogoPath(AppID appID) const
Definition: steampp.cpp:338
std::string getAppHeroPath(AppID appID) const
Definition: steampp.cpp:355
std::string getAppInstallDir(AppID appID) const
Definition: steampp.cpp:314
std::string_view getInstallDir() const
Definition: steampp.cpp:286
std::string getAppIconPath(AppID appID) const
Definition: steampp.cpp:321
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