Skip to content

Commit 365273b

Browse files
fspp: initial work on a filesystem library
1 parent 6795542 commit 365273b

File tree

7 files changed

+368
-1
lines changed

7 files changed

+368
-1
lines changed

CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
1818
option(SOURCEPP_LIBS_START_ENABLED "Libraries will all build by default" ON)
1919
option(SOURCEPP_USE_BSPPP "Build bsppp library" ${SOURCEPP_LIBS_START_ENABLED})
2020
option(SOURCEPP_USE_DMXPP "Build dmxpp library" ${SOURCEPP_LIBS_START_ENABLED})
21+
option(SOURCEPP_USE_FSPP "Build fspp library" ${SOURCEPP_LIBS_START_ENABLED})
2122
option(SOURCEPP_USE_GAMEPP "Build gamepp library" ${SOURCEPP_LIBS_START_ENABLED})
2223
option(SOURCEPP_USE_KVPP "Build kvpp library" ${SOURCEPP_LIBS_START_ENABLED})
2324
option(SOURCEPP_USE_MDLPP "Build mdlpp library" ${SOURCEPP_LIBS_START_ENABLED})
@@ -48,6 +49,12 @@ option(SOURCEPP_VPKPP_SUPPORT_VPK_V54 "Support compressed v54 VPKs" ON)
4849
if(SOURCEPP_USE_BSPPP)
4950
set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "" FORCE)
5051
endif()
52+
if(SOURCEPP_USE_FSPP)
53+
set(SOURCEPP_USE_BSPPP ON CACHE INTERNAL "" FORCE)
54+
set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE)
55+
set(SOURCEPP_USE_STEAMPP ON CACHE INTERNAL "" FORCE)
56+
set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "" FORCE)
57+
endif()
5158
if(SOURCEPP_USE_STEAMPP)
5259
set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE)
5360
endif()
@@ -188,6 +195,7 @@ endif()
188195
# Add libraries
189196
add_sourcepp_library(bsppp NO_TEST ) # sourcepp::bsppp
190197
add_sourcepp_library(dmxpp ) # sourcepp::dmxpp
198+
add_sourcepp_library(fspp ) # sourcepp::fspp
191199
add_sourcepp_library(gamepp C PYTHON ) # sourcepp::gamepp
192200
add_sourcepp_library(kvpp BENCH) # sourcepp::kvpp
193201
add_sourcepp_library(mdlpp ) # sourcepp::mdlpp
@@ -257,7 +265,7 @@ endif()
257265

258266
# Print options
259267
print_options(OPTIONS
260-
USE_BSPPP USE_DMXPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP
268+
USE_BSPPP USE_DMXPP USE_FSPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP
261269
BUILD_BENCHMARKS BUILD_C_WRAPPERS BUILD_CSHARP_WRAPPERS BUILD_PYTHON_WRAPPERS BUILD_WITH_OPENCL BUILD_WITH_TBB BUILD_WITH_THREADS BUILD_TESTS BUILD_WIN7_COMPAT
262270
LINK_STATIC_MSVC_RUNTIME
263271
VPKPP_SUPPORT_VPK_V54)

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
3737
<td rowspan="1" align="center"></td>
3838
</tr>
3939
<tr><!-- empty row to disable github striped bg color --></tr>
40+
<tr>
41+
<td rowspan="1"><code>fspp</code><sup>*</sup></td>
42+
<td>(WIP) Source 1 filesystem accessor</td>
43+
<td align="center">✅</td>
44+
<td align="center">✅</td>
45+
<td rowspan="1" align="center"></td>
46+
</tr>
47+
<tr><!-- empty row to disable github striped bg color --></tr>
4048
<tr>
4149
<td rowspan="3"><code>gamepp</code></td>
4250
<td>Get Source engine instance window title/position/size</td>

docs/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
3535
<td align="center">❌</td>
3636
<td rowspan="1" align="center"></td>
3737
</tr>
38+
<tr>
39+
<td rowspan="1"><code>fspp</code><sup>*</sup></td>
40+
<td>(WIP) Source 1 filesystem accessor</td>
41+
<td align="center">✅</td>
42+
<td align="center">✅</td>
43+
<td rowspan="1" align="center"></td>
44+
</tr>
3845
<tr>
3946
<td rowspan="2"><code>gamepp</code></td>
4047
<td>Get Source engine instance window title/position/size</td>

include/fspp/fspp.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#pragma once
2+
3+
#include <optional>
4+
5+
#include <steampp/steampp.h>
6+
#include <vpkpp/vpkpp.h>
7+
8+
namespace fspp {
9+
10+
#if defined(_WIN32)
11+
constexpr std::string_view DEFAULT_PLATFORM = "win64";
12+
#elif defined(__APPLE__)
13+
constexpr std::string_view DEFAULT_PLATFORM = "osx64";
14+
#elif defined(__linux__)
15+
constexpr std::string_view DEFAULT_PLATFORM = "linux64";
16+
#else
17+
#warning "Unknown platform! Leaving the default platform blank..."
18+
constexpr std::string_view DEFAULT_PLATFORM = "";
19+
#endif
20+
21+
struct FileSystemOptions {
22+
std::string binPlatform{DEFAULT_PLATFORM};
23+
//std::string language{}; // todo: add a <path>_<language> dir or <path>_<language>_dir.vpk for each GAME path
24+
//bool loadPakXXVPKs = true; // todo: load pakXX_dir.vpk for each dir path
25+
//bool loadSteamMounts = true; // todo: cfg/mounts.kv, the mounts block in gameinfo (Strata)
26+
//bool loadAddonList = false; // todo: addonlist.txt (L4D2), addonlist.kv3 (Strata)
27+
//bool useDLCFolders = true; // todo: dlc1, dlc2, etc.
28+
//bool useUpdate = true; // todo: mount update folder on GAME/MOD with highest priority
29+
//bool useXLSPPatch = true; // todo: mount xlsppatch folder on GAME/MOD with highester priority
30+
};
31+
32+
class FileSystem {
33+
public:
34+
using SearchPathMapDir = std::unordered_map<std::string, std::vector<std::string>>;
35+
using SearchPathMapVPK = std::unordered_map<std::string, std::vector<std::unique_ptr<vpkpp::PackFile>>>;
36+
37+
/**
38+
* Creates a FileSystem based on a Steam installation
39+
* @param appID The AppID of the base game
40+
* @param gameID The name of the directory where gameinfo.txt is located (e.g. "portal2")
41+
* @param options FileSystem creation options
42+
* @return The created FileSystem if the specified Steam game is installed
43+
*/
44+
[[nodiscard]] static std::optional<FileSystem> load(steampp::AppID appID, std::string_view gameID, const FileSystemOptions& options = {});
45+
46+
/**
47+
* Creates a FileSystem based on a local installation
48+
* @param gamePath The full path to the directory where gameinfo.txt is located (e.g. "path/to/portal2")
49+
* @param options FileSystem creation options
50+
* @return The created FileSystem if gameinfo.txt is found
51+
*/
52+
[[nodiscard]] static std::optional<FileSystem> load(std::string_view gamePath, const FileSystemOptions& options = {});
53+
54+
[[nodiscard]] const SearchPathMapDir& getSearchPathDirs() const;
55+
56+
[[nodiscard]] SearchPathMapDir& getSearchPathDirs();
57+
58+
[[nodiscard]] const SearchPathMapVPK& getSearchPathVPKs() const;
59+
60+
[[nodiscard]] SearchPathMapVPK& getSearchPathVPKs();
61+
62+
[[nodiscard]] std::optional<std::vector<std::byte>> read(std::string_view filePath, std::string_view searchPath = "GAME", bool prioritizeVPKs = true) const;
63+
64+
[[nodiscard]] std::optional<std::vector<std::byte>> readForMap(const vpkpp::PackFile* map, std::string_view filePath, std::string_view searchPath = "GAME", bool prioritizeVPKs = true) const;
65+
66+
protected:
67+
explicit FileSystem(std::string_view gamePath, const FileSystemOptions& options = {});
68+
69+
private:
70+
std::string rootPath;
71+
SearchPathMapDir searchPathDirs;
72+
SearchPathMapVPK searchPathVPKs;
73+
};
74+
75+
} // namespace fspp

src/fspp/_fspp.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
add_pretty_parser(fspp
2+
DEPS sourcepp::kvpp sourcepp::steampp
3+
DEPS_PUBLIC sourcepp::bsppp sourcepp::vpkpp
4+
SOURCES
5+
"${CMAKE_CURRENT_SOURCE_DIR}/include/fspp/fspp.h"
6+
"${CMAKE_CURRENT_LIST_DIR}/fspp.cpp")

src/fspp/fspp.cpp

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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+
11+
using namespace bsppp;
12+
using namespace fspp;
13+
using namespace kvpp;
14+
using namespace sourcepp;
15+
using namespace steampp;
16+
using namespace vpkpp;
17+
18+
namespace {
19+
20+
[[nodiscard]] std::string getAppInstallDir(AppID appID) {
21+
static Steam steam;
22+
return steam.getAppInstallDir(appID);
23+
}
24+
25+
} // namespace
26+
27+
std::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+
35+
std::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+
42+
FileSystem::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+
}
73+
string::normalizeSlashes(path);
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+
184+
const FileSystem::SearchPathMapDir& FileSystem::getSearchPathDirs() const {
185+
return this->searchPathDirs;
186+
}
187+
188+
FileSystem::SearchPathMapDir& FileSystem::getSearchPathDirs() {
189+
return this->searchPathDirs;
190+
}
191+
192+
const FileSystem::SearchPathMapVPK& FileSystem::getSearchPathVPKs() const {
193+
return this->searchPathVPKs;
194+
}
195+
196+
FileSystem::SearchPathMapVPK& FileSystem::getSearchPathVPKs() {
197+
return this->searchPathVPKs;
198+
}
199+
200+
std::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+
238+
std::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+
}

0 commit comments

Comments
 (0)