SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
fspp.cpp
Go to the documentation of this file.
1#include <fspp/fspp.h>
2
3#include <filesystem>
4
5#include <bsppp/bsppp.h>
6#include <kvpp/kvpp.h>
7#include <sourcepp/FS.h>
8#include <sourcepp/String.h>
9#include <vpkpp/vpkpp.h>
10
11using namespace bsppp;
12using namespace fspp;
13using namespace kvpp;
14using namespace sourcepp;
15using namespace steampp;
16using namespace vpkpp;
17
18namespace {
19
20[[nodiscard]] std::string getAppInstallDir(AppID appID) {
21 static Steam steam;
22 return steam.getAppInstallDir(appID);
23}
24
25} // namespace
26
27std::optional<FileSystem> FileSystem::load(steampp::AppID appID, std::string_view gameID, const FileSystemOptions& options) {
28 const auto gamePath = ::getAppInstallDir(appID);
29 if (gamePath.empty()) {
30 return std::nullopt;
31 }
32 return load((std::filesystem::path{gamePath} / gameID).string(), options);
33}
34
35std::optional<FileSystem> FileSystem::load(std::string_view gamePath, const FileSystemOptions& options) {
36 if (!std::filesystem::exists(std::filesystem::path{gamePath} / "gameinfo.txt") || !std::filesystem::is_regular_file(std::filesystem::path{gamePath} / "gameinfo.txt")) {
37 return std::nullopt;
38 }
39 return FileSystem{gamePath, options};
40}
41
42FileSystem::FileSystem(std::string_view gamePath, const FileSystemOptions& options)
43 : rootPath(std::filesystem::path{gamePath}.parent_path().string()) {
44 string::normalizeSlashes(this->rootPath);
45 const auto gameID = std::filesystem::path{gamePath}.filename().string();
46
47 // Load gameinfo.txt
48 KV1 gameinfo{fs::readFileText((std::filesystem::path{gamePath} / "gameinfo.txt").string())};
49 if (gameinfo.getChildCount() == 0) {
50 return;
51 }
52
53 // Load searchpaths
54 const auto& searchPathKVs = gameinfo[0]["FileSystem"]["SearchPaths"];
55 if (searchPathKVs.isInvalid()) {
56 return;
57 }
58 for (int i = 0; i < searchPathKVs.getChildCount(); i++) {
59 auto searches = string::split(string::toLower(searchPathKVs[i].getKey()), '+');
60 auto path = std::string{string::toLower(searchPathKVs[i].getValue())};
61
62 // Replace |all_source_engine_paths| with "", |gameinfo_path| with "<game>/"
63 static constexpr std::string_view ALL_SOURCE_ENGINE_PATHS = "|all_source_engine_paths|";
64 static constexpr std::string_view GAMEINFO_PATH = "|gameinfo_path|";
65 if (path.starts_with(ALL_SOURCE_ENGINE_PATHS)) {
66 path = path.substr(ALL_SOURCE_ENGINE_PATHS.length());
67 } else if (path.starts_with(GAMEINFO_PATH)) {
68 path = gameID + '/' + path.substr(GAMEINFO_PATH.length());
69 }
70 if (path.ends_with(".") && !path.ends_with("..")) {
71 path.pop_back();
72 }
74
75 if (path.ends_with(".vpk")) {
76 auto fullPath = this->rootPath + '/' + path;
77
78 // Normalize the ending (add _dir if present)
79 if (!std::filesystem::exists(fullPath)) {
80 auto fullPathWithDir = (std::filesystem::path{fullPath}.parent_path() / std::filesystem::path{fullPath}.stem()).string() + "_dir.vpk";
81 if (!std::filesystem::exists(fullPathWithDir)) {
82 continue;
83 }
84 fullPath = fullPathWithDir;
85 }
86
87 // Add the VPK search path
88 for (const auto& search : searches) {
89 if (!this->searchPathVPKs.contains(search)) {
90 this->searchPathVPKs[search] = std::vector<std::unique_ptr<PackFile>>{};
91 }
92 auto packFile = PackFile::open(fullPath);
93 if (packFile) {
94 this->searchPathVPKs[search].push_back(std::move(packFile));
95 }
96 }
97 } else {
98 for (const auto& search : searches) {
99 if (!this->searchPathDirs.contains(search)) {
100 this->searchPathDirs[search] = {};
101 }
102 if (path.ends_with("/*")) {
103 // Add the glob dir searchpath
104 if (const auto globParentPath = this->rootPath + '/' + path.substr(0, path.length() - 2); std::filesystem::exists(globParentPath) && std::filesystem::is_directory(globParentPath)) {
105 for (const auto directoryIterator : std::filesystem::directory_iterator{globParentPath, std::filesystem::directory_options::skip_permission_denied}) {
106 auto globChildPath = std::filesystem::relative(directoryIterator.path(), this->rootPath).string();
107 string::normalizeSlashes(globChildPath);
108 this->searchPathDirs[search].push_back(globChildPath);
109 }
110 }
111 } else if (std::filesystem::exists(this->rootPath + '/' + path)) {
112 // Add the dir searchpath
113 this->searchPathDirs[search].push_back(path);
114
115 if (search == "game") {
116 // Add dir/bin to GAMEBIN searchpath
117 if (!this->searchPathDirs.contains("gamebin")) {
118 this->searchPathDirs["gamebin"] = {};
119 }
120 this->searchPathDirs["gamebin"].push_back(path + "/bin");
121
122 if (i == 0) {
123 // Add dir to MOD searchpath
124 if (!this->searchPathDirs.contains("mod")) {
125 this->searchPathDirs["mod"] = {};
126 }
127 this->searchPathDirs["mod"].push_back(path);
128 }
129 }
130 }
131
132 // todo: Add the pakXX_dir VPK searchpath(s) if they exist
133 }
134 }
135 }
136
137 // todo: Add DLCs / update dir / xlsppatch dir if they exist
138
139 // Add EXECUTABLE_PATH if it doesn't exist, point it at "bin/<platform>"; "bin"; ""
140 if (!this->searchPathDirs.contains("executable_path")) {
141 if (!options.binPlatform.empty() && std::filesystem::exists(std::filesystem::path{this->rootPath} / "bin" / options.binPlatform)) {
142 this->searchPathDirs["executable_path"] = {"bin/" + options.binPlatform};
143 } else {
144 this->searchPathDirs["executable_path"] = {};
145 }
146 this->searchPathDirs["executable_path"].push_back("bin");
147 this->searchPathDirs["executable_path"].push_back("");
148 }
149
150 // Add PLATFORM if it doesn't exist, point it at "platform"
151 if (!this->searchPathDirs.contains("platform")) {
152 this->searchPathDirs["platform"] = {"platform"};
153 }
154
155 // Add PLATFORM path to GAME searchpath as well
156 if (this->searchPathDirs.contains("game")) {
157 bool foundPlatform = false;
158 for (const auto& path : this->searchPathDirs["game"]) {
159 if (path == "platform") {
160 foundPlatform = true;
161 }
162 }
163 if (!foundPlatform) {
164 this->searchPathDirs["game"].push_back("platform");
165 }
166 }
167
168 // Add DEFAULT_WRITE_PATH if it doesn't exist, point it at "<game>"
169 if (!this->searchPathDirs.contains("default_write_path")) {
170 this->searchPathDirs["default_write_path"] = {gameID};
171 }
172
173 // Add LOGDIR if it doesn't exist, point it at "<game>"
174 if (!this->searchPathDirs.contains("logdir")) {
175 this->searchPathDirs["logdir"] = {gameID};
176 }
177
178 // Add CONFIG if it doesn't exist, point it at "platform/config"
179 if (!this->searchPathDirs.contains("config")) {
180 this->searchPathDirs["config"] = {"platform/config"};
181 }
182}
183
185 return this->searchPathDirs;
186}
187
189 return this->searchPathDirs;
190}
191
193 return this->searchPathVPKs;
194}
195
197 return this->searchPathVPKs;
198}
199
200std::optional<std::vector<std::byte>> FileSystem::read(std::string_view filePath, std::string_view searchPath, bool prioritizeVPKs) const {
201 std::string filePathStr = string::toLower(filePath);
202 string::normalizeSlashes(filePathStr, true);
203 std::string searchPathStr = string::toLower(searchPath);
204
205 const auto checkVPKs = [this, &filePathStr, &searchPathStr]() -> std::optional<std::vector<std::byte>> {
206 if (!this->searchPathVPKs.contains(searchPathStr)) {
207 return std::nullopt;
208 }
209 for (const auto& packFile : this->searchPathVPKs.at(searchPathStr)) {
210 if (packFile->hasEntry(filePathStr)) {
211 return packFile->readEntry(filePathStr);
212 }
213 }
214 return std::nullopt;
215 };
216
217 if (prioritizeVPKs) {
218 if (auto data = checkVPKs()) {
219 return data;
220 }
221 }
222
223 if (this->searchPathDirs.contains(searchPathStr)) {
224 for (const auto& basePath : this->searchPathDirs.at(searchPathStr)) {
225 // todo: case insensitivity on Linux
226 if (const auto testPath = this->rootPath + '/' + basePath + '/' + filePathStr; std::filesystem::exists(testPath)) {
227 return fs::readFileBuffer(testPath);
228 }
229 }
230 }
231
232 if (!prioritizeVPKs) {
233 return checkVPKs();
234 }
235 return std::nullopt;
236}
237
238std::optional<std::vector<std::byte>> FileSystem::readForMap(const PackFile* map, std::string_view filePath, std::string_view searchPath, bool prioritizeVPKs) const {
239 if (const auto filePathStr = std::string{filePath}; map && map->hasEntry(filePathStr)) {
240 return map->readEntry(filePathStr);
241 }
242 return this->read(filePath, searchPath, prioritizeVPKs);
243}
static std::optional< FileSystem > load(steampp::AppID appID, std::string_view gameID, const FileSystemOptions &options={})
Creates a FileSystem based on a Steam installation.
Definition: fspp.cpp:27
std::optional< std::vector< std::byte > > readForMap(const vpkpp::PackFile *map, std::string_view filePath, std::string_view searchPath="GAME", bool prioritizeVPKs=true) const
Definition: fspp.cpp:238
const SearchPathMapVPK & getSearchPathVPKs() const
Definition: fspp.cpp:192
const SearchPathMapDir & getSearchPathDirs() const
Definition: fspp.cpp:184
std::unordered_map< std::string, std::vector< std::string > > SearchPathMapDir
Definition: fspp.h:34
std::unordered_map< std::string, std::vector< std::unique_ptr< vpkpp::PackFile > > > SearchPathMapVPK
Definition: fspp.h:35
FileSystem(std::string_view gamePath, const FileSystemOptions &options={})
Definition: fspp.cpp:42
std::optional< std::vector< std::byte > > read(std::string_view filePath, std::string_view searchPath="GAME", bool prioritizeVPKs=true) const
Definition: fspp.cpp:200
Definition: KV1.h:189
std::string getAppInstallDir(AppID appID) const
Definition: steampp.cpp:286
bool hasEntry(const std::string &path, bool includeUnbaked=true) const
Check if an entry exists given the file path.
Definition: PackFile.cpp:159
virtual std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const =0
Try to read the entry's data to a bytebuffer.
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a generic pack file. The parser is selected based on the file extension.
Definition: PackFile.cpp:114
Definition: BSP.h:18
Definition: fspp.h:8
Definition: KV1.h:13
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
void normalizeSlashes(std::string &path, bool stripSlashPrefix=false, bool stripSlashSuffix=true)
Definition: String.cpp:206
std::vector< std::string > split(std::string_view s, char delim)
Definition: String.cpp:135
void toLower(std::string &input)
Definition: String.cpp:145
Definition: LZMA.h:11
Based on SteamAppPathProvider.
Definition: steampp.h:15
uint32_t AppID
Definition: steampp.h:17
Definition: Attribute.h:5
std::string binPlatform
Definition: fspp.h:22