Skip to content

Commit

Permalink
feat(fspp): initial work on a filesystem library
Browse files Browse the repository at this point in the history
  • Loading branch information
craftablescience committed Jan 19, 2025
1 parent 6795542 commit ae20000
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 1 deletion.
10 changes: 9 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
75 changes: 75 additions & 0 deletions include/fspp/fspp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#pragma once

#include <optional>

#include <steampp/steampp.h>
#include <vpkpp/vpkpp.h>

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 <path>_<language> dir or <path>_<language>_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<std::string, std::vector<std::string>>;
using SearchPathMapVPK = std::unordered_map<std::string, std::vector<std::unique_ptr<vpkpp::PackFile>>>;

/**
* 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<FileSystem> 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<FileSystem> 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<std::vector<std::byte>> read(std::string_view filePath, std::string_view searchPath = "GAME", bool prioritizeVPKs = true) const;

[[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;

protected:
explicit FileSystem(std::string_view gamePath, const FileSystemOptions& options = {});

private:
std::string rootPath;
SearchPathMapDir searchPathDirs;
SearchPathMapVPK searchPathVPKs;
};

} // namespace fspp
6 changes: 6 additions & 0 deletions src/fspp/_fspp.cmake
Original file line number Diff line number Diff line change
@@ -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")
243 changes: 243 additions & 0 deletions src/fspp/fspp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#include <fspp/fspp.h>

#include <filesystem>

#include <bsppp/bsppp.h>
#include <kvpp/kvpp.h>
#include <sourcepp/FS.h>
#include <sourcepp/String.h>
#include <vpkpp/vpkpp.h>

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> 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> 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 "<game>/"
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<std::unique_ptr<PackFile>>{};
}
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/<platform>"; "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 "<game>"
if (!this->searchPathDirs.contains("default_write_path")) {
this->searchPathDirs["default_write_path"] = {gameID};
}

// Add LOGDIR if it doesn't exist, point it at "<game>"
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<std::vector<std::byte>> 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<std::vector<std::byte>> {
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<std::vector<std::byte>> 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);
}
20 changes: 20 additions & 0 deletions test/fspp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <gtest/gtest.h>

#include <fspp/fspp.h>

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

0 comments on commit ae20000

Please sign in to comment.