Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
<td rowspan="1" align="center"></td>
</tr>
<tr><!-- empty row to disable github striped bg color --></tr>
<tr>
<td rowspan="1"><code>fspp</code><sup>*</sup></td>
<td>(WIP) Source 1 filesystem accessor</td>
<td align="center">✅</td>
<td align="center">✅</td>
<td rowspan="1" align="center"></td>
</tr>
<tr><!-- empty row to disable github striped bg color --></tr>
<tr>
<td rowspan="3"><code>gamepp</code></td>
<td>Get Source engine instance window title/position/size</td>
Expand Down
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
<td align="center">❌</td>
<td rowspan="1" align="center"></td>
</tr>
<tr>
<td rowspan="1"><code>fspp</code><sup>*</sup></td>
<td>(WIP) Source 1 filesystem accessor</td>
<td align="center">✅</td>
<td align="center">✅</td>
<td rowspan="1" align="center"></td>
</tr>
<tr>
<td rowspan="2"><code>gamepp</code></td>
<td>Get Source engine instance window title/position/size</td>
Expand Down
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);
}
Loading
Loading