Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add gameinfo parser / filesystem accessor #9

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 All @@ -37,6 +38,11 @@ option(SOURCEPP_LINK_STATIC_MSVC_RUNTIME "Link to static MSVC runtime library" O


# Option overrides
if(SOURCEPP_USE_FSPP)
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 @@ -119,6 +125,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 ) # sourcepp::gamepp
add_sourcepp_library(kvpp ) # sourcepp::kvpp
add_sourcepp_library(mdlpp ) # sourcepp::mdlpp
Expand Down Expand Up @@ -149,6 +156,6 @@ 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_WITH_OPENCL BUILD_WITH_TBB BUILD_TESTS BUILD_WIN7_COMPAT
LINK_STATIC_MSVC_RUNTIME)
62 changes: 62 additions & 0 deletions include/fspp/fspp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#pragma once

#include <optional>

#include <steampp/steampp.h>

namespace vpkpp {

class BSP;

} // namespace vpkpp

namespace fspp {

struct FileSystemOptions {
std::string language;
bool prioritizeVPKs = true;
//bool loadAddonList = false;
//bool useDLCFolders = false;
//bool useUpdate = true;
//bool useXLSPPatch = true;
};

class FileSystem {
public:
using SearchPathMap = std::unordered_map<std::string, std::vector<std::string>>;

/**
* Creates a FileSystem based on a Steam installation
* @param appID The AppID of the base game
* @param gameName 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 gameName, 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]] std::vector<std::string> getSearchPaths() const;

[[nodiscard]] const std::vector<std::string>& getPathsForSearchPath(std::string_view searchPath) const;

[[nodiscard]] const SearchPathMap& getSearchPathData() const;

[[nodiscard]] std::optional<std::vector<std::byte>> read(std::string_view filePath, std::string_view searchPath = "GAME") const;

[[nodiscard]] std::optional<std::vector<std::byte>> readForMap(vpkpp::BSP& map, std::string_view filePath, std::string_view searchPath = "GAME") const;

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

private:
SearchPathMap searchPaths;
};

} // namespace fspp
5 changes: 5 additions & 0 deletions src/fspp/_fspp.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
add_pretty_parser(fspp
DEPS kvpp steampp vpkpp
SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/include/fspp/fspp.h"
"${CMAKE_CURRENT_LIST_DIR}/fspp.cpp")
153 changes: 153 additions & 0 deletions src/fspp/fspp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include <fspp/fspp.h>

#include <filesystem>
#include <ranges>

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

using namespace fspp;
using namespace kvpp;
using namespace sourcepp;
using namespace steampp;
using namespace vpkpp;

namespace {

#if defined(_WIN32)
constexpr std::string_view FS_PLATFORM = "win64";
#elif defined(__APPLE__)
constexpr std::string_view FS_PLATFORM = "osx64";
#elif defined(__linux__)
constexpr std::string_view FS_PLATFORM = "linux64";
#else
#error "Unknown platform!"
#endif

[[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 gameName, const FileSystemOptions& options) {
auto gamePath = ::getAppInstallDir(appID);
if (gamePath.empty()) {
return std::nullopt;
}
return load((std::filesystem::path{gamePath} / gameName).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) {
SearchPathMap dirSearchPaths;
SearchPathMap vpkSearchPaths;

// Load paths from gameinfo.txt
KV1 gameinfo{fs::readFileText((std::filesystem::path{gamePath} / "gameinfo.txt").string())};
if (gameinfo.getChildCount() == 0) {
return;
}
const auto& searchPathKVs = gameinfo[0]["FileSystem"]["SearchPaths"];
if (searchPathKVs.isInvalid()) {
return;
}
for (int i = 0; i < searchPathKVs.getChildCount(); i++) {
auto searches = string::split(searchPathKVs.getKey(), '+');
auto path = std::string{searchPathKVs[i].getValue()};

// Replace |all_source_engine_paths| with <root>/, |gameinfo_path| with <root>/<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 = (std::filesystem::path{gamePath} / ".." / path.substr(ALL_SOURCE_ENGINE_PATHS.length())).string();
} else if (path.starts_with(GAMEINFO_PATH)) {
path = (std::filesystem::path{gamePath} / path.substr(GAMEINFO_PATH.length())).string();
}

if (path.ends_with(".vpk")) {
// Normalize the ending (add _dir if present)
if (!std::filesystem::exists(path)) {
auto pathWithDir = (std::filesystem::path{path}.parent_path() / std::filesystem::path{path}.stem()).string() + "_dir.vpk";
if (!std::filesystem::exists(pathWithDir)) {
continue;
}
path = pathWithDir;
}

for (const auto& search : searches) {
if (!vpkSearchPaths.contains(search)) {
vpkSearchPaths[search] = {};
}
vpkSearchPaths[search].push_back(path);
}
} else {
for (const auto& search : searches) {
if (!dirSearchPaths.contains(search)) {
dirSearchPaths[search] = {};
}
dirSearchPaths[search].push_back(path);
}
}
}

// Add DLCs / update dir / xlsppatch dir if they exist

// Add EXECUTABLE_PATH if it doesn't exist, point it at <root>/bin/<platform>/;<root>/bin/;<root>/

// Add PLATFORM if it doesn't exist, point it at <root>/platform/

// Add DEFAULT_WRITE_PATH, LOGDIR if they doesn't exist, point them at <root>/<game>/

// Add CONFIG if it doesn't exist, point it at <root>/platform/config/

// Merge dir/vpk search paths together
const auto* firstSearchPathsMap = options.prioritizeVPKs ? &vpkSearchPaths : &dirSearchPaths;
const auto* secondSearchPathsMap = options.prioritizeVPKs ? &dirSearchPaths : &vpkSearchPaths;
for (const auto& [search, paths] : *firstSearchPathsMap) {
this->searchPaths[search] = paths;
}
for (const auto& [search, paths] : *secondSearchPathsMap) {
if (this->searchPaths.contains(search)) {
// insert
} else {
this->searchPaths[search] = paths;
}
}
}

std::vector<std::string> FileSystem::getSearchPaths() const {
auto keys = std::views::keys(this->searchPaths);
return {keys.begin(), keys.end()};
}

const std::vector<std::string>& FileSystem::getPathsForSearchPath(std::string_view searchPath) const {
return this->searchPaths.at(std::string{searchPath});
}

const FileSystem::SearchPathMap& FileSystem::getSearchPathData() const {
return this->searchPaths;
}

std::optional<std::vector<std::byte>> FileSystem::read(std::string_view filePath, std::string_view searchPath) const {
if (!this->searchPaths.contains(std::string{searchPath})) {
return std::nullopt;
}
return std::nullopt;
}

std::optional<std::vector<std::byte>> FileSystem::readForMap(BSP& map, std::string_view filePath, std::string_view searchPath) const {
if (!this->searchPaths.contains(std::string{searchPath})) {
return std::nullopt;
}
return std::nullopt;
}
15 changes: 15 additions & 0 deletions test/fspp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#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);
}

#endif