diff --git a/CMakeLists.txt b/CMakeLists.txt
index a7c4fdd51..569a47e25 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(SOURCEPP_LIBS_START_ENABLED "Libraries will all build by default" ON)
option(SOURCEPP_USE_BSPPP "Build bsppp library" ${SOURCEPP_LIBS_START_ENABLED})
option(SOURCEPP_USE_DMXPP "Build dmxpp library" ${SOURCEPP_LIBS_START_ENABLED})
+option(SOURCEPP_USE_FSPP "Build fspp library" ${SOURCEPP_LIBS_START_ENABLED})
option(SOURCEPP_USE_GAMEPP "Build gamepp library" ${SOURCEPP_LIBS_START_ENABLED})
option(SOURCEPP_USE_KVPP "Build kvpp library" ${SOURCEPP_LIBS_START_ENABLED})
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)
if(SOURCEPP_USE_BSPPP)
set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "" FORCE)
endif()
+if(SOURCEPP_USE_FSPP)
+ set(SOURCEPP_USE_BSPPP ON CACHE INTERNAL "" FORCE)
+ set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE)
+ set(SOURCEPP_USE_STEAMPP ON CACHE INTERNAL "" FORCE)
+ set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "" FORCE)
+endif()
if(SOURCEPP_USE_STEAMPP)
set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE)
endif()
@@ -188,6 +195,7 @@ endif()
# Add libraries
add_sourcepp_library(bsppp NO_TEST ) # sourcepp::bsppp
add_sourcepp_library(dmxpp ) # sourcepp::dmxpp
+add_sourcepp_library(fspp ) # sourcepp::fspp
add_sourcepp_library(gamepp C PYTHON ) # sourcepp::gamepp
add_sourcepp_library(kvpp BENCH) # sourcepp::kvpp
add_sourcepp_library(mdlpp ) # sourcepp::mdlpp
@@ -257,7 +265,7 @@ endif()
# Print options
print_options(OPTIONS
- USE_BSPPP USE_DMXPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP
+ USE_BSPPP USE_DMXPP USE_FSPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP
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
LINK_STATIC_MSVC_RUNTIME
VPKPP_SUPPORT_VPK_V54)
diff --git a/README.md b/README.md
index 72a49178e..24782283d 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
|
+
+ fspp* |
+ (WIP) Source 1 filesystem accessor |
+ ✅ |
+ ✅ |
+ |
+
+
gamepp |
Get Source engine instance window title/position/size |
diff --git a/docs/index.md b/docs/index.md
index 6b5aa2152..d81e3dea1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -35,6 +35,13 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
❌ |
|
+
+ fspp* |
+ (WIP) Source 1 filesystem accessor |
+ ✅ |
+ ✅ |
+ |
+
gamepp |
Get Source engine instance window title/position/size |
diff --git a/include/fspp/fspp.h b/include/fspp/fspp.h
new file mode 100644
index 000000000..ecd69d9f4
--- /dev/null
+++ b/include/fspp/fspp.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace fspp {
+
+#if defined(_WIN32)
+ constexpr std::string_view DEFAULT_PLATFORM = "win64";
+#elif defined(__APPLE__)
+ constexpr std::string_view DEFAULT_PLATFORM = "osx64";
+#elif defined(__linux__)
+ constexpr std::string_view DEFAULT_PLATFORM = "linux64";
+#else
+ #warning "Unknown platform! Leaving the default platform blank..."
+ constexpr std::string_view DEFAULT_PLATFORM = "";
+#endif
+
+struct FileSystemOptions {
+ std::string binPlatform{DEFAULT_PLATFORM};
+ //std::string language{}; // todo: add a _ dir or __dir.vpk for each GAME path
+ //bool loadPakXXVPKs = true; // todo: load pakXX_dir.vpk for each dir path
+ //bool loadSteamMounts = true; // todo: cfg/mounts.kv, the mounts block in gameinfo (Strata)
+ //bool loadAddonList = false; // todo: addonlist.txt (L4D2), addonlist.kv3 (Strata)
+ //bool useDLCFolders = true; // todo: dlc1, dlc2, etc.
+ //bool useUpdate = true; // todo: mount update folder on GAME/MOD with highest priority
+ //bool useXLSPPatch = true; // todo: mount xlsppatch folder on GAME/MOD with highester priority
+};
+
+class FileSystem {
+public:
+ using SearchPathMapDir = std::unordered_map>;
+ using SearchPathMapVPK = std::unordered_map>>;
+
+ /**
+ * Creates a FileSystem based on a Steam installation
+ * @param appID The AppID of the base game
+ * @param gameID The name of the directory where gameinfo.txt is located (e.g. "portal2")
+ * @param options FileSystem creation options
+ * @return The created FileSystem if the specified Steam game is installed
+ */
+ [[nodiscard]] static std::optional load(steampp::AppID appID, std::string_view gameID, const FileSystemOptions& options = {});
+
+ /**
+ * Creates a FileSystem based on a local installation
+ * @param gamePath The full path to the directory where gameinfo.txt is located (e.g. "path/to/portal2")
+ * @param options FileSystem creation options
+ * @return The created FileSystem if gameinfo.txt is found
+ */
+ [[nodiscard]] static std::optional load(std::string_view gamePath, const FileSystemOptions& options = {});
+
+ [[nodiscard]] const SearchPathMapDir& getSearchPathDirs() const;
+
+ [[nodiscard]] SearchPathMapDir& getSearchPathDirs();
+
+ [[nodiscard]] const SearchPathMapVPK& getSearchPathVPKs() const;
+
+ [[nodiscard]] SearchPathMapVPK& getSearchPathVPKs();
+
+ [[nodiscard]] std::optional> read(std::string_view filePath, std::string_view searchPath = "GAME", bool prioritizeVPKs = true) const;
+
+ [[nodiscard]] std::optional> readForMap(const vpkpp::PackFile* map, std::string_view filePath, std::string_view searchPath = "GAME", bool prioritizeVPKs = true) const;
+
+protected:
+ explicit FileSystem(std::string_view gamePath, const FileSystemOptions& options = {});
+
+private:
+ std::string rootPath;
+ SearchPathMapDir searchPathDirs;
+ SearchPathMapVPK searchPathVPKs;
+};
+
+} // namespace fspp
diff --git a/src/fspp/_fspp.cmake b/src/fspp/_fspp.cmake
new file mode 100644
index 000000000..410957112
--- /dev/null
+++ b/src/fspp/_fspp.cmake
@@ -0,0 +1,6 @@
+add_pretty_parser(fspp
+ DEPS sourcepp::kvpp sourcepp::steampp
+ DEPS_PUBLIC sourcepp::bsppp sourcepp::vpkpp
+ SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/fspp/fspp.h"
+ "${CMAKE_CURRENT_LIST_DIR}/fspp.cpp")
diff --git a/src/fspp/fspp.cpp b/src/fspp/fspp.cpp
new file mode 100644
index 000000000..372f6b25c
--- /dev/null
+++ b/src/fspp/fspp.cpp
@@ -0,0 +1,243 @@
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+using namespace bsppp;
+using namespace fspp;
+using namespace kvpp;
+using namespace sourcepp;
+using namespace steampp;
+using namespace vpkpp;
+
+namespace {
+
+[[nodiscard]] std::string getAppInstallDir(AppID appID) {
+ static Steam steam;
+ return steam.getAppInstallDir(appID);
+}
+
+} // namespace
+
+std::optional FileSystem::load(steampp::AppID appID, std::string_view gameID, const FileSystemOptions& options) {
+ const auto gamePath = ::getAppInstallDir(appID);
+ if (gamePath.empty()) {
+ return std::nullopt;
+ }
+ return load((std::filesystem::path{gamePath} / gameID).string(), options);
+}
+
+std::optional FileSystem::load(std::string_view gamePath, const FileSystemOptions& options) {
+ if (!std::filesystem::exists(std::filesystem::path{gamePath} / "gameinfo.txt") || !std::filesystem::is_regular_file(std::filesystem::path{gamePath} / "gameinfo.txt")) {
+ return std::nullopt;
+ }
+ return FileSystem{gamePath, options};
+}
+
+FileSystem::FileSystem(std::string_view gamePath, const FileSystemOptions& options)
+ : rootPath(std::filesystem::path{gamePath}.parent_path().string()) {
+ string::normalizeSlashes(this->rootPath);
+ const auto gameID = std::filesystem::path{gamePath}.filename().string();
+
+ // Load gameinfo.txt
+ KV1 gameinfo{fs::readFileText((std::filesystem::path{gamePath} / "gameinfo.txt").string())};
+ if (gameinfo.getChildCount() == 0) {
+ return;
+ }
+
+ // Load searchpaths
+ const auto& searchPathKVs = gameinfo[0]["FileSystem"]["SearchPaths"];
+ if (searchPathKVs.isInvalid()) {
+ return;
+ }
+ for (int i = 0; i < searchPathKVs.getChildCount(); i++) {
+ auto searches = string::split(string::toLower(searchPathKVs[i].getKey()), '+');
+ auto path = std::string{string::toLower(searchPathKVs[i].getValue())};
+
+ // Replace |all_source_engine_paths| with "", |gameinfo_path| with "/"
+ static constexpr std::string_view ALL_SOURCE_ENGINE_PATHS = "|all_source_engine_paths|";
+ static constexpr std::string_view GAMEINFO_PATH = "|gameinfo_path|";
+ if (path.starts_with(ALL_SOURCE_ENGINE_PATHS)) {
+ path = path.substr(ALL_SOURCE_ENGINE_PATHS.length());
+ } else if (path.starts_with(GAMEINFO_PATH)) {
+ path = gameID + '/' + path.substr(GAMEINFO_PATH.length());
+ }
+ if (path.ends_with(".") && !path.ends_with("..")) {
+ path.pop_back();
+ }
+ string::normalizeSlashes(path);
+
+ if (path.ends_with(".vpk")) {
+ auto fullPath = this->rootPath + '/' + path;
+
+ // Normalize the ending (add _dir if present)
+ if (!std::filesystem::exists(fullPath)) {
+ auto fullPathWithDir = (std::filesystem::path{fullPath}.parent_path() / std::filesystem::path{fullPath}.stem()).string() + "_dir.vpk";
+ if (!std::filesystem::exists(fullPathWithDir)) {
+ continue;
+ }
+ fullPath = fullPathWithDir;
+ }
+
+ // Add the VPK search path
+ for (const auto& search : searches) {
+ if (!this->searchPathVPKs.contains(search)) {
+ this->searchPathVPKs[search] = std::vector>{};
+ }
+ auto packFile = PackFile::open(fullPath);
+ if (packFile) {
+ this->searchPathVPKs[search].push_back(std::move(packFile));
+ }
+ }
+ } else {
+ for (const auto& search : searches) {
+ if (!this->searchPathDirs.contains(search)) {
+ this->searchPathDirs[search] = {};
+ }
+ if (path.ends_with("/*")) {
+ // Add the glob dir searchpath
+ if (const auto globParentPath = this->rootPath + '/' + path.substr(0, path.length() - 2); std::filesystem::exists(globParentPath) && std::filesystem::is_directory(globParentPath)) {
+ for (const auto directoryIterator : std::filesystem::directory_iterator{globParentPath, std::filesystem::directory_options::skip_permission_denied}) {
+ auto globChildPath = std::filesystem::relative(directoryIterator.path(), this->rootPath).string();
+ string::normalizeSlashes(globChildPath);
+ this->searchPathDirs[search].push_back(globChildPath);
+ }
+ }
+ } else if (std::filesystem::exists(this->rootPath + '/' + path)) {
+ // Add the dir searchpath
+ this->searchPathDirs[search].push_back(path);
+
+ if (search == "game") {
+ // Add dir/bin to GAMEBIN searchpath
+ if (!this->searchPathDirs.contains("gamebin")) {
+ this->searchPathDirs["gamebin"] = {};
+ }
+ this->searchPathDirs["gamebin"].push_back(path + "/bin");
+
+ if (i == 0) {
+ // Add dir to MOD searchpath
+ if (!this->searchPathDirs.contains("mod")) {
+ this->searchPathDirs["mod"] = {};
+ }
+ this->searchPathDirs["mod"].push_back(path);
+ }
+ }
+ }
+
+ // todo: Add the pakXX_dir VPK searchpath(s) if they exist
+ }
+ }
+ }
+
+ // todo: Add DLCs / update dir / xlsppatch dir if they exist
+
+ // Add EXECUTABLE_PATH if it doesn't exist, point it at "bin/"; "bin"; ""
+ if (!this->searchPathDirs.contains("executable_path")) {
+ if (!options.binPlatform.empty() && std::filesystem::exists(std::filesystem::path{this->rootPath} / "bin" / options.binPlatform)) {
+ this->searchPathDirs["executable_path"] = {"bin/" + options.binPlatform};
+ } else {
+ this->searchPathDirs["executable_path"] = {};
+ }
+ this->searchPathDirs["executable_path"].push_back("bin");
+ this->searchPathDirs["executable_path"].push_back("");
+ }
+
+ // Add PLATFORM if it doesn't exist, point it at "platform"
+ if (!this->searchPathDirs.contains("platform")) {
+ this->searchPathDirs["platform"] = {"platform"};
+ }
+
+ // Add PLATFORM path to GAME searchpath as well
+ if (this->searchPathDirs.contains("game")) {
+ bool foundPlatform = false;
+ for (const auto& path : this->searchPathDirs["game"]) {
+ if (path == "platform") {
+ foundPlatform = true;
+ }
+ }
+ if (!foundPlatform) {
+ this->searchPathDirs["game"].push_back("platform");
+ }
+ }
+
+ // Add DEFAULT_WRITE_PATH if it doesn't exist, point it at ""
+ if (!this->searchPathDirs.contains("default_write_path")) {
+ this->searchPathDirs["default_write_path"] = {gameID};
+ }
+
+ // Add LOGDIR if it doesn't exist, point it at ""
+ if (!this->searchPathDirs.contains("logdir")) {
+ this->searchPathDirs["logdir"] = {gameID};
+ }
+
+ // Add CONFIG if it doesn't exist, point it at "platform/config"
+ if (!this->searchPathDirs.contains("config")) {
+ this->searchPathDirs["config"] = {"platform/config"};
+ }
+}
+
+const FileSystem::SearchPathMapDir& FileSystem::getSearchPathDirs() const {
+ return this->searchPathDirs;
+}
+
+FileSystem::SearchPathMapDir& FileSystem::getSearchPathDirs() {
+ return this->searchPathDirs;
+}
+
+const FileSystem::SearchPathMapVPK& FileSystem::getSearchPathVPKs() const {
+ return this->searchPathVPKs;
+}
+
+FileSystem::SearchPathMapVPK& FileSystem::getSearchPathVPKs() {
+ return this->searchPathVPKs;
+}
+
+std::optional> FileSystem::read(std::string_view filePath, std::string_view searchPath, bool prioritizeVPKs) const {
+ std::string filePathStr = string::toLower(filePath);
+ string::normalizeSlashes(filePathStr, true);
+ std::string searchPathStr = string::toLower(searchPath);
+
+ const auto checkVPKs = [this, &filePathStr, &searchPathStr]() -> std::optional> {
+ if (!this->searchPathVPKs.contains(searchPathStr)) {
+ return std::nullopt;
+ }
+ for (const auto& packFile : this->searchPathVPKs.at(searchPathStr)) {
+ if (packFile->hasEntry(filePathStr)) {
+ return packFile->readEntry(filePathStr);
+ }
+ }
+ return std::nullopt;
+ };
+
+ if (prioritizeVPKs) {
+ if (auto data = checkVPKs()) {
+ return data;
+ }
+ }
+
+ if (this->searchPathDirs.contains(searchPathStr)) {
+ for (const auto& basePath : this->searchPathDirs.at(searchPathStr)) {
+ // todo: case insensitivity on Linux
+ if (const auto testPath = this->rootPath + '/' + basePath + '/' + filePathStr; std::filesystem::exists(testPath)) {
+ return fs::readFileBuffer(testPath);
+ }
+ }
+ }
+
+ if (!prioritizeVPKs) {
+ return checkVPKs();
+ }
+ return std::nullopt;
+}
+
+std::optional> FileSystem::readForMap(const PackFile* map, std::string_view filePath, std::string_view searchPath, bool prioritizeVPKs) const {
+ if (const auto filePathStr = std::string{filePath}; map && map->hasEntry(filePathStr)) {
+ return map->readEntry(filePathStr);
+ }
+ return this->read(filePath, searchPath, prioritizeVPKs);
+}
diff --git a/test/fspp.cpp b/test/fspp.cpp
new file mode 100644
index 000000000..534da0357
--- /dev/null
+++ b/test/fspp.cpp
@@ -0,0 +1,20 @@
+#include
+
+#include
+
+using namespace fspp;
+using namespace sourcepp;
+
+#if 0
+
+TEST(fspp, open_portal2) {
+ auto fs = FileSystem::load(620, "portal2");
+ ASSERT_TRUE(fs);
+}
+
+TEST(fspp, open_p2ce) {
+ auto fs = FileSystem::load(440000, "p2ce");
+ ASSERT_TRUE(fs);
+}
+
+#endif