From b5ef6590357098d423065f6e1f1964c4e61a9db9 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Mon, 17 Jun 2024 03:25:16 -0400 Subject: [PATCH] wip(fspp): start work on filesystem library --- CMakeLists.txt | 8 ++- include/fspp/fspp.h | 54 ++++++++++++++++ src/fspp/_fspp.cmake | 3 + src/fspp/fspp.cpp | 144 +++++++++++++++++++++++++++++++++++++++++++ test/fspp.cpp | 15 +++++ 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 include/fspp/fspp.h create mode 100644 src/fspp/_fspp.cmake create mode 100644 src/fspp/fspp.cpp create mode 100644 test/fspp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dddc3a6..696c91d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Options option(SOURCEPP_USE_DMXPP "Build dmxpp library" ON) option(SOURCEPP_USE_FGDPP "Build fgdpp library" ON) +option(SOURCEPP_USE_FSPP "Build fspp library" ON) option(SOURCEPP_USE_KVPP "Build kvpp library" ON) option(SOURCEPP_USE_MDLPP "Build mdlpp library" ON) option(SOURCEPP_USE_STEAMPP "Build steampp library" ON) @@ -19,9 +20,13 @@ option(SOURCEPP_USE_STATIC_MSVC_RUNTIME "Link to static MSVC runtime library" # Option overrides -if(SOURCEPP_USE_STEAMPP OR SOURCEPP_USE_VPKPP) +if(SOURCEPP_USE_FSPP OR SOURCEPP_USE_STEAMPP OR SOURCEPP_USE_VPKPP) set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "") endif() +if(SOURCEPP_USE_FSPP) + set(SOURCEPP_USE_STEAMPP ON CACHE INTERNAL "") + set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "") +endif() # Options per-library @@ -77,6 +82,7 @@ endif() # Add libraries add_sourcepp_library(dmxpp) add_sourcepp_library(fgdpp) +add_sourcepp_library(fspp) add_sourcepp_library(kvpp) add_sourcepp_library(mdlpp) add_sourcepp_library(steampp) diff --git a/include/fspp/fspp.h b/include/fspp/fspp.h new file mode 100644 index 00000000..8d8cdf15 --- /dev/null +++ b/include/fspp/fspp.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +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>; + + /** + * 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 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 load(std::string_view gamePath, const FileSystemOptions& options = {}); + + [[nodiscard]] std::vector getSearchPaths() const; + + [[nodiscard]] const std::vector& getPathsForSearchPath(std::string_view searchPath) const; + + [[nodiscard]] const SearchPathMap& getSearchPathData() const; + + [[nodiscard]] std::optional> read(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 diff --git a/src/fspp/_fspp.cmake b/src/fspp/_fspp.cmake new file mode 100644 index 00000000..c769ec22 --- /dev/null +++ b/src/fspp/_fspp.cmake @@ -0,0 +1,3 @@ +add_pretty_parser(fspp DEPS kvpp steampp 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 00000000..043e726e --- /dev/null +++ b/src/fspp/fspp.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include + +#include +#include +#include + +using namespace fspp; +using namespace kvpp; +using namespace sourcepp; +using namespace steampp; + +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 + +std::string getAppInstallDir(AppID appID) { + static Steam steam; + return steam.getAppInstallDir(appID); +} + +} // namespace + +std::optional 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::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 /, |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 = (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 /bin//;/bin/;/ + + // Add PLATFORM if it doesn't exist, point it at /platform/ + + // Add DEFAULT_WRITE_PATH, LOGDIR if they doesn't exist, point them at // + + // Add CONFIG if it doesn't exist, point it at /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 FileSystem::getSearchPaths() const { + auto keys = std::views::keys(this->searchPaths); + return {keys.begin(), keys.end()}; +} + +const std::vector& 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> FileSystem::read(std::string_view filePath, std::string_view searchPath) const { + if (!this->searchPaths.contains(std::string{searchPath})) { + return std::nullopt; + } + return std::nullopt; +} diff --git a/test/fspp.cpp b/test/fspp.cpp new file mode 100644 index 00000000..030ef86e --- /dev/null +++ b/test/fspp.cpp @@ -0,0 +1,15 @@ +#include + +#include + +using namespace fspp; +using namespace sourcepp; + +#if 1 + +TEST(fspp, open_portal2) { + auto fs = FileSystem::load(620, "portal2"); + ASSERT_TRUE(fs); +} + +#endif