diff --git a/CMakeLists.txt b/CMakeLists.txt index 93ee9184..e98aea72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) @@ -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() @@ -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 @@ -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) diff --git a/include/fspp/fspp.h b/include/fspp/fspp.h new file mode 100644 index 00000000..0273adf2 --- /dev/null +++ b/include/fspp/fspp.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include + +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>; + + /** + * 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; + + [[nodiscard]] std::optional> 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 diff --git a/src/fspp/_fspp.cmake b/src/fspp/_fspp.cmake new file mode 100644 index 00000000..e7da7fb1 --- /dev/null +++ b/src/fspp/_fspp.cmake @@ -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") diff --git a/src/fspp/fspp.cpp b/src/fspp/fspp.cpp new file mode 100644 index 00000000..4066ae4a --- /dev/null +++ b/src/fspp/fspp.cpp @@ -0,0 +1,153 @@ +#include + +#include +#include + +#include +#include +#include +#include + +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::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; +} + +std::optional> 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; +} diff --git a/test/fspp.cpp b/test/fspp.cpp new file mode 100644 index 00000000..59b41a86 --- /dev/null +++ b/test/fspp.cpp @@ -0,0 +1,15 @@ +#include + +#include + +using namespace fspp; +using namespace sourcepp; + +#if 0 + +TEST(fspp, open_portal2) { + auto fs = FileSystem::load(620, "portal2"); + ASSERT_TRUE(fs); +} + +#endif