From 1b9d40ac662dd45db46b3bb86c6af4d797ceaf2e Mon Sep 17 00:00:00 2001 From: Skyth <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:25:05 +0300 Subject: [PATCH] Initial mod loader implementation. --- UnleashedRecomp/CMakeLists.txt | 7 +- UnleashedRecomp/kernel/io/file_system.cpp | 14 +- UnleashedRecomp/main.cpp | 3 + UnleashedRecomp/mod/ini_file.h | 46 +++++ UnleashedRecomp/mod/ini_file.inl | 205 ++++++++++++++++++++++ UnleashedRecomp/mod/mod_loader.cpp | 101 +++++++++++ UnleashedRecomp/mod/mod_loader.h | 8 + UnleashedRecomp/stdafx.h | 1 + 8 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 UnleashedRecomp/mod/ini_file.h create mode 100644 UnleashedRecomp/mod/ini_file.inl create mode 100644 UnleashedRecomp/mod/mod_loader.cpp create mode 100644 UnleashedRecomp/mod/mod_loader.h diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 04de270e..4346fa6a 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -192,6 +192,10 @@ set(SWA_INSTALL_CXX_SOURCES set(SWA_USER_CXX_SOURCES "user/achievement_data.cpp" "user/config.cpp" +) + +set(SWA_MOD_CXX_SOURCES + "mod/mod_loader.cpp" ) set(SWA_THIRDPARTY_SOURCES @@ -251,7 +255,8 @@ set(SWA_CXX_SOURCES ${SWA_PATCHES_CXX_SOURCES} ${SWA_UI_CXX_SOURCES} ${SWA_INSTALL_CXX_SOURCES} - ${SWA_USER_CXX_SOURCES} + ${SWA_USER_CXX_SOURCES} + ${SWA_MOD_CXX_SOURCES} ${SWA_THIRDPARTY_SOURCES} ) diff --git a/UnleashedRecomp/kernel/io/file_system.cpp b/UnleashedRecomp/kernel/io/file_system.cpp index 71d7f2dd..04de92a7 100644 --- a/UnleashedRecomp/kernel/io/file_system.cpp +++ b/UnleashedRecomp/kernel/io/file_system.cpp @@ -5,6 +5,7 @@ #include #include #include +#include struct FileHandle : KernelObject { @@ -36,6 +37,15 @@ struct FindHandle : KernelObject } }; +static std::filesystem::path TransformOrRedirectPath(const char* path) +{ + std::filesystem::path redirectedPath = ModLoader::RedirectPath(path); + if (redirectedPath.empty()) + redirectedPath = std::u8string_view((const char8_t*)FileSystem::TransformPath(path)); + + return redirectedPath; +} + SWA_API FileHandle* XCreateFileA ( const char* lpFileName, @@ -50,7 +60,7 @@ SWA_API FileHandle* XCreateFileA assert(((dwShareMode & ~(FILE_SHARE_READ | FILE_SHARE_WRITE)) == 0) && "Unknown share mode bits."); assert(((dwCreationDisposition & ~(CREATE_NEW | CREATE_ALWAYS)) == 0) && "Unknown creation disposition bits."); - std::filesystem::path filePath = std::u8string_view((const char8_t*)(FileSystem::TransformPath(lpFileName))); + std::filesystem::path filePath = TransformOrRedirectPath(lpFileName); std::fstream fileStream; std::ios::openmode fileOpenMode = std::ios::binary; if (dwDesiredAccess & (GENERIC_READ | FILE_READ_DATA)) @@ -305,7 +315,7 @@ uint32_t XReadFileEx(FileHandle* hFile, void* lpBuffer, uint32_t nNumberOfBytesT uint32_t XGetFileAttributesA(const char* lpFileName) { - std::filesystem::path filePath(std::u8string_view((const char8_t*)(FileSystem::TransformPath(lpFileName)))); + std::filesystem::path filePath = TransformOrRedirectPath(lpFileName); if (std::filesystem::is_directory(filePath)) return FILE_ATTRIBUTE_DIRECTORY; else if (std::filesystem::is_regular_file(filePath)) diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index bc23967a..a78559cb 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #define GAME_XEX_PATH "game:\\default.xex" @@ -80,6 +81,8 @@ void KiSystemStartup() } } + ModLoader::Init(); + XAudioInitializeSystem(); } diff --git a/UnleashedRecomp/mod/ini_file.h b/UnleashedRecomp/mod/ini_file.h new file mode 100644 index 00000000..0957e48f --- /dev/null +++ b/UnleashedRecomp/mod/ini_file.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +class IniFile +{ +protected: + struct Property + { + std::string name; + std::string value; + }; + + struct Section + { + std::string name; + xxHashMap properties; + }; + + xxHashMap
m_sections; + + static size_t hashStr(const std::string_view& str); + + static bool isWhitespace(char value); + static bool isNewLine(char value); + +public: + bool read(const std::filesystem::path& filePath); + + std::string getString(const std::string_view& sectionName, const std::string_view& propertyName, std::string defaultValue) const; + + bool getBool(const std::string_view& sectionName, const std::string_view& propertyName, bool defaultValue) const; + + template + T get(const std::string_view& sectionName, const std::string_view& propertyName, T defaultValue) const; + + template + void enumerate(const T& function) const; + + template + void enumerate(const std::string_view& sectionName, const T& function) const; + + bool contains(const std::string_view& sectionName) const; +}; + +#include "ini_file.inl" diff --git a/UnleashedRecomp/mod/ini_file.inl b/UnleashedRecomp/mod/ini_file.inl new file mode 100644 index 00000000..c92f4c4c --- /dev/null +++ b/UnleashedRecomp/mod/ini_file.inl @@ -0,0 +1,205 @@ +inline size_t IniFile::hashStr(const std::string_view& str) +{ + return XXH3_64bits(str.data(), str.size()); +} + +inline bool IniFile::isWhitespace(char value) +{ + return value == ' ' || value == '\t'; +} + +inline bool IniFile::isNewLine(char value) +{ + return value == '\n' || value == '\r'; +} + +inline bool IniFile::read(const std::filesystem::path& filePath) +{ + std::ifstream file(filePath, std::ios::binary); + if (!file.good()) + return false; + + file.seekg(0, std::ios::end); + + const size_t dataSize = static_cast(file.tellg()); + const auto data = std::make_unique(dataSize + 1); + data[dataSize] = '\0'; + + file.seekg(0, std::ios::beg); + file.read(data.get(), dataSize); + + file.close(); + + Section* section = nullptr; + const char* dataPtr = data.get(); + + while (dataPtr < data.get() + dataSize) + { + if (*dataPtr == ';') + { + while (*dataPtr != '\0' && !isNewLine(*dataPtr)) + ++dataPtr; + } + else if (*dataPtr == '[') + { + ++dataPtr; + const char* endPtr = dataPtr; + while (*endPtr != '\0' && !isNewLine(*endPtr) && *endPtr != ']') + ++endPtr; + + if (*endPtr != ']') + return false; + + std::string sectionName(dataPtr, endPtr - dataPtr); + section = &m_sections[hashStr(sectionName)]; + section->name = std::move(sectionName); + + dataPtr = endPtr + 1; + } + else if (!isWhitespace(*dataPtr) && !isNewLine(*dataPtr)) + { + if (section == nullptr) + return false; + + const char* endPtr; + if (*dataPtr == '"') + { + ++dataPtr; + endPtr = dataPtr; + + while (*endPtr != '\0' && !isNewLine(*endPtr) && *endPtr != '"') + ++endPtr; + + if (*endPtr != '"') + return false; + } + else + { + endPtr = dataPtr; + + while (*endPtr != '\0' && !isNewLine(*endPtr) && !isWhitespace(*endPtr) && *endPtr != '=') + ++endPtr; + + if (!isNewLine(*endPtr) && !isWhitespace(*endPtr) && *endPtr != '=') + return false; + } + + std::string propertyName(dataPtr, endPtr - dataPtr); + auto& property = section->properties[hashStr(propertyName)]; + property.name = std::move(propertyName); + + dataPtr = endPtr; + while (*dataPtr != '\0' && !isNewLine(*dataPtr) && *dataPtr != '=') + ++dataPtr; + + if (*dataPtr == '=') + { + ++dataPtr; + + while (*dataPtr != '\0' && isWhitespace(*dataPtr)) + ++dataPtr; + + if (*dataPtr == '"') + { + ++dataPtr; + endPtr = dataPtr; + + while (*endPtr != '\0' && !isNewLine(*endPtr) && *endPtr != '"') + ++endPtr; + + if (*endPtr != '"') + return false; + } + else + { + endPtr = dataPtr; + + while (*endPtr != '\0' && !isNewLine(*endPtr) && !isWhitespace(*endPtr)) + ++endPtr; + } + + property.value = std::string(dataPtr, endPtr - dataPtr); + dataPtr = endPtr + 1; + } + } + else + { + ++dataPtr; + } + } + + return true; +} + +inline std::string IniFile::getString(const std::string_view& sectionName, const std::string_view& propertyName, std::string defaultValue) const +{ + const auto sectionPair = m_sections.find(hashStr(sectionName)); + if (sectionPair != m_sections.end()) + { + const auto propertyPair = sectionPair->second.properties.find(hashStr(propertyName)); + if (propertyPair != sectionPair->second.properties.end()) + return propertyPair->second.value; + } + return defaultValue; +} + +inline bool IniFile::getBool(const std::string_view& sectionName, const std::string_view& propertyName, bool defaultValue) const +{ + const auto sectionPair = m_sections.find(hashStr(sectionName)); + if (sectionPair != m_sections.end()) + { + const auto propertyPair = sectionPair->second.properties.find(hashStr(propertyName)); + if (propertyPair != sectionPair->second.properties.end() && !propertyPair->second.value.empty()) + { + const char firstChar = propertyPair->second.value[0]; + return firstChar == 't' || firstChar == 'T' || firstChar == 'y' || firstChar == 'Y' || firstChar == '1'; + } + } + return defaultValue; +} + +inline bool IniFile::contains(const std::string_view& sectionName) const +{ + return m_sections.contains(hashStr(sectionName)); +} + +template +T IniFile::get(const std::string_view& sectionName, const std::string_view& propertyName, T defaultValue) const +{ + const auto sectionPair = m_sections.find(hashStr(sectionName)); + if (sectionPair != m_sections.end()) + { + const auto propertyPair = sectionPair->second.properties.find(hashStr(propertyName)); + if (propertyPair != sectionPair->second.properties.end()) + { + T value{}; + const auto result = std::from_chars(propertyPair->second.value.data(), + propertyPair->second.value.data() + propertyPair->second.value.size(), value); + + if (result.ec == std::errc{}) + return value; + } + } + return defaultValue; +} + +template +inline void IniFile::enumerate(const T& function) const +{ + for (const auto& [_, section] : m_sections) + { + for (auto& property : section.properties) + function(section.name, property.second.name, property.second.value); + } +} + +template +void IniFile::enumerate(const std::string_view& sectionName, const T& function) const +{ + const auto sectionPair = m_sections.find(hashStr(sectionName)); + if (sectionPair != m_sections.end()) + { + for (const auto& property : sectionPair->second.properties) + function(property.second.name, property.second.value); + } +} diff --git a/UnleashedRecomp/mod/mod_loader.cpp b/UnleashedRecomp/mod/mod_loader.cpp new file mode 100644 index 00000000..9983f76a --- /dev/null +++ b/UnleashedRecomp/mod/mod_loader.cpp @@ -0,0 +1,101 @@ +#include "mod_loader.h" +#include "ini_file.h" +#include "../xxHashMap.h" + +enum class ModType +{ + HMM, + UMM +}; + +struct Mod +{ + ModType type{}; + std::vector includeDirs; +}; + +static std::vector g_mods; + +std::filesystem::path ModLoader::RedirectPath(std::string_view path) +{ + if (g_mods.empty()) + return {}; + + thread_local xxHashMap pathCache; + + size_t sepIndex = path.find(":\\"); + if (sepIndex != std::string_view::npos) + path.remove_prefix(sepIndex + 2); + + XXH64_hash_t hash = XXH3_64bits(path.data(), path.size()); + auto findResult = pathCache.find(hash); + if (findResult != pathCache.end()) + return findResult->second; + + for (auto& mod : g_mods) + { + for (auto& includeDir : mod.includeDirs) + { + std::filesystem::path modPath = includeDir / path; + if (std::filesystem::exists(modPath)) + return pathCache.emplace(hash, modPath).first->second; + } + } + + return pathCache.emplace(hash, std::filesystem::path{}).first->second; +} + +void ModLoader::Init() +{ + IniFile configIni; + if (!configIni.read("cpkredir.ini")) + return; + + std::string modsDbIniFilePathU8 = configIni.getString("CPKREDIR", "ModsDbIni", ""); + if (modsDbIniFilePathU8.empty()) + return; + + IniFile modsDbIni; + if (!modsDbIni.read(std::u8string_view((const char8_t*)modsDbIniFilePathU8.c_str()))) + return; + + size_t activeModCount = modsDbIni.get("Main", "ActiveModCount", 0); + for (size_t i = 0; i < activeModCount; ++i) + { + std::string modId = modsDbIni.getString("Main", fmt::format("ActiveMod{}", i), ""); + if (modId.empty()) + continue; + + std::string modIniFilePathU8 = modsDbIni.getString("Mods", modId, ""); + if (modIniFilePathU8.empty()) + continue; + + std::filesystem::path modIniFilePath(std::u8string_view((const char8_t*)modIniFilePathU8.c_str())); + + IniFile modIni; + if (!modIni.read(modIniFilePath)) + continue; + + auto modDirectoryPath = modIniFilePath.parent_path(); + + auto& mod = g_mods.emplace_back(); + + if (modIni.contains("Details") || modIni.contains("Filesystem")) // UMM + { + mod.type = ModType::UMM; + mod.includeDirs.emplace_back(std::move(modDirectoryPath)); + } + else // HMM + { + mod.type = ModType::HMM; + + size_t includeDirCount = modIni.get("Main", "IncludeDirCount", 0); + for (size_t j = 0; j < includeDirCount; j++) + { + std::string includeDirU8 = modIni.getString("Main", fmt::format("IncludeDir{}", j), ""); + if (!includeDirU8.empty()) + mod.includeDirs.emplace_back(modDirectoryPath / std::u8string_view((const char8_t*)includeDirU8.c_str())); + } + } + } +} diff --git a/UnleashedRecomp/mod/mod_loader.h b/UnleashedRecomp/mod/mod_loader.h new file mode 100644 index 00000000..b12383ac --- /dev/null +++ b/UnleashedRecomp/mod/mod_loader.h @@ -0,0 +1,8 @@ +#pragma once + +struct ModLoader +{ + static std::filesystem::path RedirectPath(std::string_view path); + + static void Init(); +}; diff --git a/UnleashedRecomp/stdafx.h b/UnleashedRecomp/stdafx.h index 40590b8f..6bbf4181 100644 --- a/UnleashedRecomp/stdafx.h +++ b/UnleashedRecomp/stdafx.h @@ -49,6 +49,7 @@ using Microsoft::WRL::ComPtr; #include #include #include +#include #include "framework.h" #include "mutex.h"