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