From a397a90551cb870909967047adcdc0dedb4e207e Mon Sep 17 00:00:00 2001 From: "Skyth (Asilkan)" <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Tue, 31 Dec 2024 20:20:07 +0300 Subject: [PATCH] Mod loader implementation. (#66) * Initial mod loader implementation. * Allow iterating in mod directories. * Initial append archive implementation. * Avoid calling function wrappers when loading append ARs. For some reason they cause issues. Should investigate later. * UMM merge archive support. * Load merge archives without archive lists. * Less thread locals. I shouldn't worry about string allocations this much when the game itself spams them... * Check for read-only UMM archives. TODO: Skip merging as it's currently just doing duplicate loads. * Skip loading merge archives if they are read-only. * Merge only archives. * Implement decompression. * Fix append ARLs not loading. * Initial save file redirection implementation. * Slightly refactor resolved path usage. * Implement save file redirection fallback. * Set a default save file path if none is provided. * Check for enabled option & replace backward slashes with forward ones in mod save file paths. * Convert back slashes to forward ones when iterating directories. * Make CSB limit dynamic. * Cache append/merge archive lookups. * Close stream after reading compressed ARL. * Fix UMM/HMM ARL file path inconsistency. --- UnleashedRecomp/CMakeLists.txt | 7 +- UnleashedRecomp/kernel/io/file_system.cpp | 145 ++-- UnleashedRecomp/kernel/io/file_system.h | 4 +- UnleashedRecomp/kernel/memory.h | 11 +- UnleashedRecomp/kernel/xam.cpp | 2 +- UnleashedRecomp/main.cpp | 30 +- UnleashedRecomp/mod/ini_file.h | 46 ++ UnleashedRecomp/mod/ini_file.inl | 205 ++++++ UnleashedRecomp/mod/mod_loader.cpp | 781 ++++++++++++++++++++++ UnleashedRecomp/mod/mod_loader.h | 12 + UnleashedRecomp/stdafx.h | 1 + UnleashedRecomp/user/achievement_data.cpp | 11 +- UnleashedRecomp/user/achievement_data.h | 4 +- UnleashedRecomp/user/paths.h | 19 +- 14 files changed, 1195 insertions(+), 83 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 04de270..4346fa6 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 71d7f2d..0c669e6 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 { @@ -15,21 +16,62 @@ struct FileHandle : KernelObject struct FindHandle : KernelObject { std::error_code ec; - std::filesystem::path searchPath; - std::filesystem::directory_iterator iterator; + ankerl::unordered_dense::map> searchResult; // Relative path, file size, is directory + decltype(searchResult)::iterator iterator; + + FindHandle(const std::string_view& path) + { + auto addDirectory = [&](const std::filesystem::path& directory) + { + for (auto& entry : std::filesystem::directory_iterator(directory, ec)) + { + std::u8string relativePath = entry.path().lexically_relative(directory).u8string(); + searchResult.emplace(relativePath, std::make_pair(entry.is_directory(ec) ? 0 : entry.file_size(ec), entry.is_directory(ec))); + } + }; + + std::string_view pathNoPrefix = path; + size_t index = pathNoPrefix.find(":\\"); + if (index != std::string_view::npos) + pathNoPrefix.remove_prefix(index + 2); + + // Force add a work folder to let the game see the files in mods, + // if by some rare chance the user has no DLC or update files. + if (pathNoPrefix.empty()) + searchResult.emplace(u8"work", std::make_pair(0, true)); + + // Look for only work folder in mod folders, AR files cause issues. + if (pathNoPrefix.starts_with("work")) + { + std::string pathStr(pathNoPrefix); + std::replace(pathStr.begin(), pathStr.end(), '\\', '/'); + + for (size_t i = 0; ; i++) + { + auto* includeDirs = ModLoader::GetIncludeDirectories(i); + if (includeDirs == nullptr) + break; + + for (auto& includeDir : *includeDirs) + addDirectory(includeDir / pathStr); + } + } + + addDirectory(FileSystem::ResolvePath(path, false)); + + iterator = searchResult.begin(); + } void fillFindData(WIN32_FIND_DATAA* lpFindFileData) { - if (iterator->is_directory()) + if (iterator->second.second) lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_DIRECTORY); - else if (iterator->is_regular_file()) + else lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_NORMAL); - std::u8string pathU8Str = iterator->path().lexically_relative(searchPath).u8string(); - uint64_t fileSize = iterator->file_size(ec); - strncpy(lpFindFileData->cFileName, (const char *)(pathU8Str.c_str()), sizeof(lpFindFileData->cFileName)); - lpFindFileData->nFileSizeLow = ByteSwap(uint32_t(fileSize >> 32U)); - lpFindFileData->nFileSizeHigh = ByteSwap(uint32_t(fileSize)); + strncpy(lpFindFileData->cFileName, (const char *)(iterator->first.c_str()), sizeof(lpFindFileData->cFileName)); + lpFindFileData->nFileSizeLow = ByteSwap(uint32_t(iterator->second.first >> 32U)); + lpFindFileData->nFileSizeHigh = ByteSwap(uint32_t(iterator->second.first)); lpFindFileData->ftCreationTime = {}; lpFindFileData->ftLastAccessTime = {}; lpFindFileData->ftLastWriteTime = {}; @@ -50,7 +92,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 = FileSystem::ResolvePath(lpFileName, true); std::fstream fileStream; std::ios::openmode fileOpenMode = std::ios::binary; if (dwDesiredAccess & (GENERIC_READ | FILE_READ_DATA)) @@ -228,45 +270,35 @@ uint32_t XSetFilePointerEx(FileHandle* hFile, int32_t lDistanceToMove, LARGE_INT FindHandle* XFindFirstFileA(const char* lpFileName, WIN32_FIND_DATAA* lpFindFileData) { - const char *transformedPath = FileSystem::TransformPath(lpFileName); - size_t transformedPathLength = strlen(transformedPath); - if (transformedPathLength == 0) - return (FindHandle*)GUEST_INVALID_HANDLE_VALUE; - - std::filesystem::path dirPath; - if (strstr(transformedPath, "\\*") == (&transformedPath[transformedPathLength - 2]) || strstr(transformedPath, "/*") == (&transformedPath[transformedPathLength - 2])) + std::string_view path = lpFileName; + if (path.find("\\*") == (path.size() - 2) || path.find("/*") == (path.size() - 2)) { - dirPath = std::u8string_view((const char8_t*)(transformedPath), transformedPathLength - 2); + path.remove_suffix(1); } - else if (strstr(transformedPath, "\\*.*") == (&transformedPath[transformedPathLength - 4]) || strstr(transformedPath, "/*.*") == (&transformedPath[transformedPathLength - 4])) + else if (path.find("\\*.*") == (path.size() - 4) || path.find("/*.*") == (path.size() - 4)) { - dirPath = std::u8string_view((const char8_t *)(transformedPath), transformedPathLength - 4); + path.remove_suffix(3); } else { - dirPath = std::u8string_view((const char8_t *)(transformedPath), transformedPathLength); - assert(!dirPath.has_extension() && "Unknown search pattern."); + assert(!std::filesystem::path(path).has_extension() && "Unknown search pattern."); } - if (!std::filesystem::is_directory(dirPath)) + FindHandle findHandle(path); + + if (findHandle.searchResult.empty()) return GetInvalidKernelObject(); - std::filesystem::directory_iterator dirIterator(dirPath); - if (dirIterator == std::filesystem::directory_iterator()) - return GetInvalidKernelObject(); + findHandle.fillFindData(lpFindFileData); - FindHandle *findHandle = CreateKernelObject(); - findHandle->searchPath = std::move(dirPath); - findHandle->iterator = std::move(dirIterator); - findHandle->fillFindData(lpFindFileData); - return findHandle; + return CreateKernelObject(std::move(findHandle)); } uint32_t XFindNextFileA(FindHandle* Handle, WIN32_FIND_DATAA* lpFindFileData) { Handle->iterator++; - if (Handle->iterator == std::filesystem::directory_iterator()) + if (Handle->iterator == Handle->searchResult.end()) { return FALSE; } @@ -305,7 +337,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 = FileSystem::ResolvePath(lpFileName, true); if (std::filesystem::is_directory(filePath)) return FILE_ATTRIBUTE_DIRECTORY; else if (std::filesystem::is_regular_file(filePath)) @@ -328,52 +360,41 @@ uint32_t XWriteFile(FileHandle* hFile, const void* lpBuffer, uint32_t nNumberOfB return TRUE; } -static void fixSlashes(char *path) +std::filesystem::path FileSystem::ResolvePath(const std::string_view& path, bool checkForMods) { - while (*path != 0) + if (checkForMods) { - if (*path == '\\') - { - *path = '/'; - } - - path++; + std::filesystem::path resolvedPath = ModLoader::ResolvePath(path); + if (!resolvedPath.empty()) + return resolvedPath; } -} -const char* FileSystem::TransformPath(const char* path) -{ - thread_local char builtPath[2048]{}; - const char* relativePath = strstr(path, ":\\"); - if (relativePath != nullptr) + thread_local std::string builtPath; + builtPath.clear(); + + size_t index = path.find(":\\"); + if (index != std::string::npos) { // rooted folder, handle direction - const std::string_view root = std::string_view{ path, path + (relativePath - path) }; + const std::string_view root = path.substr(0, index); const auto newRoot = XamGetRootPath(root); if (!newRoot.empty()) { - strncpy(builtPath, newRoot.data(), newRoot.size()); - builtPath[newRoot.size()] = '\\'; - strcpy(builtPath + newRoot.size() + 1, relativePath + 2); - } - else - { - strncpy(builtPath, relativePath + 2, sizeof(builtPath)); + builtPath += newRoot; + builtPath += '/'; } + + builtPath += path.substr(index + 2); } else { - strncpy(builtPath, path, sizeof(builtPath)); + builtPath += path; } - fixSlashes(builtPath); - return builtPath; -} + std::replace(builtPath.begin(), builtPath.end(), '\\', '/'); -SWA_API const char* XExpandFilePathA(const char* path) -{ - return FileSystem::TransformPath(path); + return std::u8string_view((const char8_t*)builtPath.c_str()); } GUEST_FUNCTION_HOOK(sub_82BD4668, XCreateFileA); diff --git a/UnleashedRecomp/kernel/io/file_system.h b/UnleashedRecomp/kernel/io/file_system.h index bbe5aef..86c73b7 100644 --- a/UnleashedRecomp/kernel/io/file_system.h +++ b/UnleashedRecomp/kernel/io/file_system.h @@ -2,5 +2,5 @@ struct FileSystem { - static const char* TransformPath(const char* path); -}; \ No newline at end of file + static std::filesystem::path ResolvePath(const std::string_view& path, bool checkForMods); +}; diff --git a/UnleashedRecomp/kernel/memory.h b/UnleashedRecomp/kernel/memory.h index 98349a4..2411b19 100644 --- a/UnleashedRecomp/kernel/memory.h +++ b/UnleashedRecomp/kernel/memory.h @@ -19,6 +19,11 @@ public: void* Commit(size_t offset, size_t size); void* Reserve(size_t offset, size_t size); + bool IsInMemoryRange(const void* host) const noexcept + { + return host >= base && host < (base + size); + } + void* Translate(size_t offset) const noexcept { if (offset) @@ -27,12 +32,12 @@ public: return base + offset; } - uint32_t MapVirtual(void* host) const noexcept + uint32_t MapVirtual(const void* host) const noexcept { if (host) - assert(host >= base && host < (base + size)); + assert(IsInMemoryRange(host)); - return static_cast(static_cast(host) - base); + return static_cast(static_cast(host) - base); } }; diff --git a/UnleashedRecomp/kernel/xam.cpp b/UnleashedRecomp/kernel/xam.cpp index 2173203..66a3d67 100644 --- a/UnleashedRecomp/kernel/xam.cpp +++ b/UnleashedRecomp/kernel/xam.cpp @@ -308,7 +308,7 @@ SWA_API uint32_t XamContentCreateEx(uint32_t dwUserIndex, const char* szRootName if (pContentData->dwContentType == XCONTENTTYPE_SAVEDATA) { - std::u8string savePathU8 = GetSavePath().u8string(); + std::u8string savePathU8 = GetSavePath(true).u8string(); root = (const char *)(savePathU8.c_str()); } else if (pContentData->dwContentType == XCONTENTTYPE_DLC) diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index bc23967..cf87aa5 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #define GAME_XEX_PATH "game:\\default.xex" @@ -53,13 +54,26 @@ void KiSystemStartup() XamRegisterContent(gameContent, GAME_INSTALL_DIRECTORY "/game"); XamRegisterContent(updateContent, GAME_INSTALL_DIRECTORY "/update"); - const auto savePath = GetSavePath(); - const auto saveName = "SYS-DATA"; + const auto saveFilePath = GetSaveFilePath(true); + bool saveFileExists = std::filesystem::exists(saveFilePath); - if (std::filesystem::exists(savePath / saveName)) + if (!saveFileExists) { - std::u8string savePathU8 = savePath.u8string(); - XamRegisterContent(XamMakeContent(XCONTENTTYPE_SAVEDATA, saveName), (const char *)(savePathU8.c_str())); + // Copy base save data to modded save as fallback. + std::error_code ec; + std::filesystem::create_directories(saveFilePath.parent_path(), ec); + + if (!ec) + { + std::filesystem::copy_file(GetSaveFilePath(false), saveFilePath, ec); + saveFileExists = !ec; + } + } + + if (saveFileExists) + { + std::u8string savePathU8 = saveFilePath.parent_path().u8string(); + XamRegisterContent(XamMakeContent(XCONTENTTYPE_SAVEDATA, "SYS-DATA"), (const char*)(savePathU8.c_str())); } // Mount game @@ -175,12 +189,14 @@ int main(int argc, char *argv[]) } } + ModLoader::Init(); + AchievementData::Load(); KiSystemStartup(); - const char *modulePath = FileSystem::TransformPath(GAME_XEX_PATH); - uint32_t entry = LdrLoadModule(std::u8string_view((const char8_t*)(modulePath))); + auto modulePath = FileSystem::ResolvePath(GAME_XEX_PATH, false); + uint32_t entry = LdrLoadModule(modulePath); if (!runInstallerWizard) Video::CreateHostDevice(sdlVideoDriver); diff --git a/UnleashedRecomp/mod/ini_file.h b/UnleashedRecomp/mod/ini_file.h new file mode 100644 index 0000000..0957e48 --- /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 0000000..c92f4c4 --- /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 0000000..216b20f --- /dev/null +++ b/UnleashedRecomp/mod/mod_loader.cpp @@ -0,0 +1,781 @@ +#include "mod_loader.h" +#include "ini_file.h" + +#include +#include +#include +#include +#include + +enum class ModType +{ + HMM, + UMM +}; + +struct Mod +{ + ModType type{}; + std::vector includeDirs; + bool merge = false; + ankerl::unordered_dense::set readOnly; +}; + +static std::vector g_mods; + +std::filesystem::path ModLoader::ResolvePath(std::string_view path) +{ + if (g_mods.empty()) + return {}; + + std::string_view root; + + size_t sepIndex = path.find(":\\"); + if (sepIndex != std::string_view::npos) + { + root = path.substr(0, sepIndex); + path.remove_prefix(sepIndex + 2); + } + + if (root == "save") + { + if (!ModLoader::s_saveFilePath.empty()) + { + if (path == "SYS-DATA") + return ModLoader::s_saveFilePath; + else + return ModLoader::s_saveFilePath.parent_path() / path; + } + + return {}; + } + + thread_local xxHashMap s_cache; + + XXH64_hash_t hash = XXH3_64bits(path.data(), path.size()); + auto findResult = s_cache.find(hash); + if (findResult != s_cache.end()) + return findResult->second; + + std::string pathStr(path); + std::replace(pathStr.begin(), pathStr.end(), '\\', '/'); + std::filesystem::path fsPath(std::move(pathStr)); + + bool canBeMerged = + path.find(".arl") == (path.size() - 4) || + path.find(".ar.") == (path.size() - 6) || + path.find(".ar") == (path.size() - 3); + + for (auto& mod : g_mods) + { + if (mod.type == ModType::UMM && mod.merge && canBeMerged && !mod.readOnly.contains(fsPath)) + continue; + + for (auto& includeDir : mod.includeDirs) + { + std::filesystem::path modPath = includeDir / fsPath; + if (std::filesystem::exists(modPath)) + return s_cache.emplace(hash, modPath).first->second; + } + } + + return s_cache.emplace(hash, std::filesystem::path{}).first->second; +} + +std::vector* ModLoader::GetIncludeDirectories(size_t modIndex) +{ + return modIndex < g_mods.size() ? &g_mods[modIndex].includeDirs : nullptr; +} + +void ModLoader::Init() +{ + IniFile configIni; + if (!configIni.read("cpkredir.ini")) + return; + + if (!configIni.getBool("CPKREDIR", "Enabled", true)) + return; + + if (configIni.getBool("CPKREDIR", "EnableSaveFileRedirection", false)) + { + std::string saveFilePathU8 = configIni.getString("CPKREDIR", "SaveFileFallback", ""); + if (!saveFilePathU8.empty()) + ModLoader::s_saveFilePath = std::u8string_view((const char8_t*)saveFilePathU8.c_str()); + else + ModLoader::s_saveFilePath = "mlsave/SYS-DATA"; + } + + 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; + + bool foundModSaveFilePath = false; + + 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(); + std::string modSaveFilePathU8; + + Mod mod; + + if (modIni.contains("Details") || modIni.contains("Filesystem")) // UMM + { + mod.type = ModType::UMM; + mod.includeDirs.emplace_back(modDirectoryPath); + mod.merge = modIni.getBool("Details", "Merge", modIni.getBool("Filesystem", "Merge", false)); + + std::string readOnly = modIni.getString("Details", "Read-only", modIni.getString("Filesystem", "Read-only", std::string())); + std::replace(readOnly.begin(), readOnly.end(), '\\', '/'); + std::string_view readOnlySplit = readOnly; + + while (!readOnlySplit.empty()) + { + size_t index = readOnlySplit.find(','); + if (index == std::string_view::npos) + { + mod.readOnly.emplace(readOnlySplit); + break; + } + + mod.readOnly.emplace(readOnlySplit.substr(0, index)); + readOnlySplit.remove_prefix(index + 1); + } + + if (!foundModSaveFilePath) + modSaveFilePathU8 = modIni.getString("Details", "Save", modIni.getString("Filesystem", "Save", std::string())); + } + 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())); + } + + if (!foundModSaveFilePath) + modSaveFilePathU8 = modIni.getString("Main", "SaveFile", std::string()); + } + + if (!mod.includeDirs.empty()) + g_mods.emplace_back(std::move(mod)); + + if (!modSaveFilePathU8.empty()) + { + std::replace(modSaveFilePathU8.begin(), modSaveFilePathU8.end(), '\\', '/'); + ModLoader::s_saveFilePath = modDirectoryPath / std::u8string_view((const char8_t*)modSaveFilePathU8.c_str()); + foundModSaveFilePath = true; + } + } +} + +static constexpr uint32_t LZX_SIGNATURE = 0xFF512EE; + +static std::span decompressLzx(PPCContext& ctx, uint8_t* base, const uint8_t* compressedData, size_t compressedDataSize, be* scratchSpace) +{ + assert(g_memory.IsInMemoryRange(compressedData)); + + bool shouldFreeScratchSpace = false; + if (scratchSpace == nullptr) + { + scratchSpace = reinterpret_cast*>(g_userHeap.Alloc(sizeof(uint32_t) * 2)); + shouldFreeScratchSpace = true; + } + + // Initialize decompressor + ctx.r3.u32 = 1; + ctx.r4.u32 = uint32_t((compressedData + 0xC) - base); + ctx.r5.u32 = *reinterpret_cast*>(compressedData + 0x8); + ctx.r6.u32 = uint32_t(reinterpret_cast(scratchSpace) - base); + sub_831CE1A0(ctx, base); + + uint64_t decompressedDataSize = *reinterpret_cast*>(compressedData + 0x18); + uint8_t* decompressedData = reinterpret_cast(g_userHeap.Alloc(decompressedDataSize)); + + uint32_t blockSize = *reinterpret_cast*>(compressedData + 0x28); + size_t decompressedDataOffset = 0; + size_t compressedDataOffset = 0x30; + + while (decompressedDataOffset < decompressedDataSize) + { + size_t decompressedBlockSize = decompressedDataSize - decompressedDataOffset; + + if (decompressedBlockSize > blockSize) + decompressedBlockSize = blockSize; + + *(scratchSpace + 1) = decompressedBlockSize; + + uint32_t compressedBlockSize = *reinterpret_cast*>(compressedData + compressedDataOffset); + + // Decompress + ctx.r3.u32 = *scratchSpace; + ctx.r4.u32 = uint32_t((decompressedData + decompressedDataOffset) - base); + ctx.r5.u32 = uint32_t(reinterpret_cast(scratchSpace + 1) - base); + ctx.r6.u32 = uint32_t((compressedData + compressedDataOffset + 0x4) - base); + ctx.r7.u32 = compressedBlockSize; + sub_831CE0D0(ctx, base); + + decompressedDataOffset += *(scratchSpace + 1); + compressedDataOffset += 0x4 + compressedBlockSize; + } + + // Deinitialize decompressor + ctx.r3.u32 = *scratchSpace; + sub_831CE150(ctx, base); + + if (shouldFreeScratchSpace) + g_userHeap.Free(scratchSpace); + + return { decompressedData, decompressedDataSize }; +} + +// Hedgehog::Database::CDatabaseLoader::ReadArchiveList +PPC_FUNC_IMPL(__imp__sub_82E0D3E8); +PPC_FUNC(sub_82E0D3E8) +{ + if (g_mods.empty()) + { + __imp__sub_82E0D3E8(ctx, base); + return; + } + + thread_local ankerl::unordered_dense::set s_fileNames; + s_fileNames.clear(); + + auto parseArlFileData = [&](const uint8_t* arlFileData, size_t arlFileSize) + { + struct ArlHeader + { + uint32_t signature; + uint32_t splitCount; + }; + + auto* arlHeader = reinterpret_cast(arlFileData); + size_t arlHeaderSize = sizeof(ArlHeader) + arlHeader->splitCount * sizeof(uint32_t); + const uint8_t* arlFileNames = arlFileData + arlHeaderSize; + + while (arlFileNames < arlFileData + arlFileSize) + { + uint8_t fileNameSize = *arlFileNames; + ++arlFileNames; + + s_fileNames.emplace(reinterpret_cast(arlFileNames), fileNameSize); + + arlFileNames += fileNameSize; + } + + return arlHeaderSize; + }; + + auto parseArFileData = [&](const uint8_t* arFileData, size_t arFileSize) + { + struct ArEntry + { + uint32_t entrySize; + uint32_t dataSize; + uint32_t dataOffset; + uint32_t fileDateLow; + uint32_t fileDateHigh; + }; + + for (size_t i = 16; i < arFileSize; ) + { + auto entry = reinterpret_cast(arFileData + i); + s_fileNames.emplace(reinterpret_cast(entry + 1)); + i += entry->entrySize; + } + }; + + auto r3 = ctx.r3; + auto r4 = ctx.r4; + auto r5 = ctx.r5; + auto r6 = ctx.r6; + + auto loadFile = [&](const std::filesystem::path& filePath, const TFunction& function) + { + std::ifstream stream(filePath, std::ios::binary); + if (stream.good()) + { + be signature{}; + stream.read(reinterpret_cast(&signature), sizeof(signature)); + + stream.seekg(0, std::ios::end); + size_t arlFileSize = stream.tellg(); + stream.seekg(0, std::ios::beg); + + if (signature == LZX_SIGNATURE) + { + void* compressedFileData = g_userHeap.Alloc(arlFileSize); + stream.read(reinterpret_cast(compressedFileData), arlFileSize); + stream.close(); + + auto fileData = decompressLzx(ctx, base, reinterpret_cast(compressedFileData), arlFileSize, nullptr); + + g_userHeap.Free(compressedFileData); + + function(fileData.data(), fileData.size()); + + g_userHeap.Free(fileData.data()); + } + else + { + thread_local std::vector s_fileData; + + s_fileData.resize(arlFileSize); + stream.read(reinterpret_cast(s_fileData.data()), arlFileSize); + stream.close(); + + function(s_fileData.data(), arlFileSize); + } + + return true; + } + + return false; + }; + + thread_local xxHashMap>> s_cache; + + std::u8string_view arlFilePathU8(reinterpret_cast(base + PPC_LOAD_U32(ctx.r4.u32))); + XXH64_hash_t hash = XXH3_64bits(arlFilePathU8.data(), arlFilePathU8.size()); + auto findResult = s_cache.find(hash); + + if (findResult != s_cache.end()) + { + for (const auto& [arlFilePath, isArchiveList] : findResult->second) + { + if (isArchiveList) + loadFile(arlFilePath, parseArlFileData); + else + loadFile(arlFilePath, parseArFileData); + } + } + else + { + std::vector> arlFilePaths; + std::filesystem::path arlFilePath; + std::filesystem::path arFilePath; + std::filesystem::path appendArlFilePath; + + for (auto& mod : g_mods) + { + for (auto& includeDir : mod.includeDirs) + { + auto loadUncachedFile = [&](const std::filesystem::path& filePath, bool isArchiveList) + { + if (mod.type == ModType::UMM && mod.readOnly.contains(filePath)) + return false; + + std::filesystem::path combinedFilePath = includeDir / filePath; + + bool success; + if (isArchiveList) + success = loadFile(combinedFilePath, parseArlFileData); + else + success = loadFile(combinedFilePath, parseArFileData); + + if (success) + arlFilePaths.emplace_back(std::move(combinedFilePath), isArchiveList); + + return success; + }; + + if (mod.type == ModType::UMM) + { + if (mod.merge) + { + if (arlFilePath.empty()) + { + arlFilePath = arlFilePathU8; + arlFilePath += ".arl"; + } + + if (!loadUncachedFile(arlFilePath, true)) + { + if (arFilePath.empty()) + { + arFilePath = arlFilePathU8; + arFilePath += ".ar"; + } + + if (!loadUncachedFile(arFilePath, false)) + { + thread_local std::filesystem::path s_tempPath; + + for (uint32_t i = 0; ; i++) + { + s_tempPath = arFilePath; + s_tempPath += fmt::format(".{:02}", i); + + if (!loadUncachedFile(s_tempPath, false)) + break; + } + } + } + } + } + else if (mod.type == ModType::HMM) + { + if (appendArlFilePath.empty()) + { + if (arlFilePath.empty()) + { + arlFilePath = arlFilePathU8; + arlFilePath += ".arl"; + } + + appendArlFilePath = arlFilePath.parent_path(); + appendArlFilePath /= "+"; + appendArlFilePath += arlFilePath.filename(); + } + + loadUncachedFile(appendArlFilePath, true); + } + } + } + + s_cache.emplace(hash, std::move(arlFilePaths)); + } + + ctx.r3 = r3; + ctx.r4 = r4; + ctx.r5 = r5; + ctx.r6 = r6; + + if (s_fileNames.empty()) + { + __imp__sub_82E0D3E8(ctx, base); + return; + } + + size_t arlHeaderSize = parseArlFileData(base + ctx.r5.u32, ctx.r6.u32); + size_t arlFileSize = arlHeaderSize; + + for (auto& fileName : s_fileNames) + { + arlFileSize += 1; + arlFileSize += fileName.size(); + } + + uint8_t* newArlFileData = reinterpret_cast(g_userHeap.Alloc(arlFileSize)); + memcpy(newArlFileData, base + ctx.r5.u32, arlHeaderSize); + + uint8_t* arlFileNames = newArlFileData + arlHeaderSize; + for (auto& fileName : s_fileNames) + { + *arlFileNames = uint8_t(fileName.size()); + ++arlFileNames; + memcpy(arlFileNames, fileName.data(), fileName.size()); + arlFileNames += fileName.size(); + } + + ctx.r5.u32 = uint32_t(newArlFileData - base); + ctx.r6.u32 = uint32_t(arlFileSize); + + __imp__sub_82E0D3E8(ctx, base); + + g_userHeap.Free(newArlFileData); +} + +// Hedgehog::Database::SLoadElement::SLoadElement +PPC_FUNC_IMPL(__imp__sub_82E140D8); +PPC_FUNC(sub_82E140D8) +{ + // Store archive name as the pretty name to use it later for append archive loading. + // This is always set to an empty string for archives, so it should be safe to replace. + if (!g_mods.empty() && PPC_LOAD_U32(ctx.r5.u32) == 0x8200A621) + ctx.r5.u32 = ctx.r6.u32; + + __imp__sub_82E140D8(ctx, base); +} + +// Hedgehog::Database::CDatabaseLoader::CCreateFromArchive::CreateCallback +PPC_FUNC_IMPL(__imp__sub_82E0B500); +PPC_FUNC(sub_82E0B500) +{ + if (g_mods.empty()) + { + __imp__sub_82E0B500(ctx, base); + return; + } + + std::u8string_view arFilePathU8(reinterpret_cast(base + PPC_LOAD_U32(ctx.r5.u32))); + size_t index = arFilePathU8.find(u8".ar.00"); + if (index == (arFilePathU8.size() - 6)) + { + arFilePathU8.remove_suffix(3); + } + else + { + index = arFilePathU8.find(u8".ar"); + + if (index != (arFilePathU8.size() - 3) || + arFilePathU8.starts_with(u8"tg-") || + arFilePathU8.starts_with(u8"gia-") || + arFilePathU8.starts_with(u8"gi-texture-")) + { + __imp__sub_82E0B500(ctx, base); + return; + } + } + + auto r3 = ctx.r3; // Callback + auto r4 = ctx.r4; // Database + auto r5 = ctx.r5; // Name + auto r6 = ctx.r6; // Data + auto r7 = ctx.r7; // Size + auto r8 = ctx.r8; // Callback data + + auto loadArchive = [&](const std::filesystem::path& arFilePath) + { + std::ifstream stream(arFilePath, std::ios::binary); + if (stream.good()) + { + stream.seekg(0, std::ios::end); + size_t arFileSize = stream.tellg(); + + void* arFileData = g_userHeap.Alloc(arFileSize); + stream.seekg(0, std::ios::beg); + stream.read(reinterpret_cast(arFileData), arFileSize); + stream.close(); + + auto arFileDataHolder = reinterpret_cast*>(g_userHeap.Alloc(sizeof(uint32_t) * 2)); + + if (*reinterpret_cast*>(arFileData) == LZX_SIGNATURE) + { + auto fileData = decompressLzx(ctx, base, reinterpret_cast(arFileData), arFileSize, arFileDataHolder); + + g_userHeap.Free(arFileData); + + arFileData = fileData.data(); + arFileSize = fileData.size(); + } + + arFileDataHolder[0] = g_memory.MapVirtual(arFileData); + arFileDataHolder[1] = NULL; + + ctx.r3 = r3; + ctx.r4 = r4; + ctx.r5 = r5; + ctx.r6.u32 = g_memory.MapVirtual(arFileDataHolder); + ctx.r7.u32 = uint32_t(arFileSize); + ctx.r8 = r8; + + __imp__sub_82E0B500(ctx, base); + + g_userHeap.Free(arFileDataHolder); + g_userHeap.Free(arFileData); + + return true; + } + + return false; + }; + + thread_local xxHashMap> s_cache; + + XXH64_hash_t hash = XXH3_64bits(arFilePathU8.data(), arFilePathU8.size()); + auto findResult = s_cache.find(hash); + if (findResult != s_cache.end()) + { + for (const auto& arFilePath : findResult->second) + loadArchive(arFilePath); + } + else + { + std::vector arFilePaths; + std::filesystem::path arFilePath; + std::filesystem::path appendArFilePath; + + for (auto& mod : g_mods) + { + for (auto& includeDir : mod.includeDirs) + { + auto loadUncachedArchive = [&](const std::filesystem::path& arFilePath) + { + if (mod.type == ModType::UMM && mod.readOnly.contains(arFilePath)) + return false; + + std::filesystem::path combinedFilePath = includeDir / arFilePath; + bool success = loadArchive(combinedFilePath); + if (success) + arFilePaths.emplace_back(std::move(combinedFilePath)); + + return success; + }; + + auto loadArchives = [&](const std::filesystem::path& arFilePath) + { + thread_local std::filesystem::path s_tempPath; + s_tempPath = arFilePath; + s_tempPath += "l"; + + if (mod.type == ModType::UMM && mod.readOnly.contains(s_tempPath)) + return; + + std::ifstream stream(includeDir / s_tempPath, std::ios::binary); + if (stream.good()) + { + be signature{}; + uint32_t splitCount{}; + stream.read(reinterpret_cast(&signature), sizeof(signature)); + + if (signature == LZX_SIGNATURE) + { + stream.seekg(0, std::ios::end); + size_t arlFileSize = stream.tellg(); + stream.seekg(0, std::ios::beg); + + void* compressedFileData = g_userHeap.Alloc(arlFileSize); + stream.read(reinterpret_cast(compressedFileData), arlFileSize); + stream.close(); + + auto fileData = decompressLzx(ctx, base, reinterpret_cast(compressedFileData), arlFileSize, nullptr); + + g_userHeap.Free(compressedFileData); + + splitCount = *reinterpret_cast(fileData.data() + 0x4); + + g_userHeap.Free(fileData.data()); + } + else + { + stream.read(reinterpret_cast(&splitCount), sizeof(splitCount)); + stream.close(); + } + + if (splitCount == 0) + { + loadUncachedArchive(arFilePath); + } + else + { + for (uint32_t i = 0; i < splitCount; i++) + { + s_tempPath = arFilePath; + s_tempPath += fmt::format(".{:02}", i); + loadUncachedArchive(s_tempPath); + } + } + } + else if (mod.type == ModType::UMM) + { + if (!loadUncachedArchive(arFilePath)) + { + for (uint32_t i = 0; ; i++) + { + s_tempPath = arFilePath; + s_tempPath += fmt::format(".{:02}", i); + if (!loadUncachedArchive(s_tempPath)) + break; + } + } + } + }; + + if (mod.type == ModType::UMM) + { + if (mod.merge) + { + if (arFilePath.empty()) + arFilePath = arFilePathU8; + + loadArchives(arFilePath); + } + } + else if (mod.type == ModType::HMM) + { + if (appendArFilePath.empty()) + { + if (arFilePath.empty()) + arFilePath = arFilePathU8; + + appendArFilePath = arFilePath.parent_path(); + appendArFilePath /= "+"; + appendArFilePath += arFilePath.filename(); + } + + loadArchives(appendArFilePath); + } + } + } + + s_cache.emplace(hash, std::move(arFilePaths)); + } + + ctx.r3 = r3; + ctx.r4 = r4; + ctx.r5 = r5; + ctx.r6 = r6; + ctx.r7 = r7; + ctx.r8 = r8; + + __imp__sub_82E0B500(ctx, base); +} + +// CriAuObjLoc::AttachCueSheet +PPC_FUNC_IMPL(__imp__sub_8314A310); +PPC_FUNC(sub_8314A310) +{ + // allocator: 0x4 + // capacity: 0x24 + // count: 0x28 + // data: 0x2C + uint32_t capacity = PPC_LOAD_U32(ctx.r3.u32 + 0x24); + if (capacity == PPC_LOAD_U32(ctx.r3.u32 + 0x28)) + { + auto r3 = ctx.r3; + auto r4 = ctx.r4; + auto r5 = ctx.r5; + + // Allocate + ctx.r3.u32 = PPC_LOAD_U32(r3.u32 + 0x4); + ctx.r4.u32 = (capacity * 2) * sizeof(uint32_t); + ctx.r5.u32 = 0x82195248; // AuObjCueSheet + ctx.r6.u32 = 0x4; + sub_83167FD8(ctx, base); + + // Copy + uint32_t oldData = PPC_LOAD_U32(r3.u32 + 0x2C); + uint32_t newData = ctx.r3.u32; + + memcpy(base + newData, base + oldData, capacity * sizeof(uint32_t)); + memset(base + newData + (capacity * sizeof(uint32_t)), 0, capacity * sizeof(uint32_t)); + + PPC_STORE_U32(r3.u32 + 0x24, capacity * 2); + PPC_STORE_U32(r3.u32 + 0x2C, newData); + + // Deallocate + ctx.r3.u32 = PPC_LOAD_U32(r3.u32 + 0x4); + ctx.r4.u32 = oldData; + sub_83168100(ctx, base); + + ctx.r3 = r3; + ctx.r4 = r4; + ctx.r5 = r5; + } + + __imp__sub_8314A310(ctx, base); +} diff --git a/UnleashedRecomp/mod/mod_loader.h b/UnleashedRecomp/mod/mod_loader.h new file mode 100644 index 0000000..84a5830 --- /dev/null +++ b/UnleashedRecomp/mod/mod_loader.h @@ -0,0 +1,12 @@ +#pragma once + +struct ModLoader +{ + static inline std::filesystem::path s_saveFilePath; + + static std::filesystem::path ResolvePath(std::string_view path); + + static std::vector* GetIncludeDirectories(size_t modIndex); + + static void Init(); +}; diff --git a/UnleashedRecomp/stdafx.h b/UnleashedRecomp/stdafx.h index 40590b8..6bbf418 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" diff --git a/UnleashedRecomp/user/achievement_data.cpp b/UnleashedRecomp/user/achievement_data.cpp index 00c1296..4b9e25b 100644 --- a/UnleashedRecomp/user/achievement_data.cpp +++ b/UnleashedRecomp/user/achievement_data.cpp @@ -104,10 +104,15 @@ bool AchievementData::VerifyChecksum() void AchievementData::Load() { - auto dataPath = GetDataPath(); + auto dataPath = GetDataPath(true); if (!std::filesystem::exists(dataPath)) - return; + { + // Try loading base achievement data as fallback. + dataPath = GetDataPath(false); + if (!std::filesystem::exists(dataPath)) + return; + } std::ifstream file(dataPath, std::ios::binary); @@ -160,7 +165,7 @@ void AchievementData::Load() void AchievementData::Save() { - std::ofstream file(GetDataPath(), std::ios::binary); + std::ofstream file(GetDataPath(true), std::ios::binary); if (!file) { diff --git a/UnleashedRecomp/user/achievement_data.h b/UnleashedRecomp/user/achievement_data.h index 2a23c28..ec99519 100644 --- a/UnleashedRecomp/user/achievement_data.h +++ b/UnleashedRecomp/user/achievement_data.h @@ -45,9 +45,9 @@ public: static inline Data Data{ ACH_SIGNATURE, ACH_VERSION }; - static std::filesystem::path GetDataPath() + static std::filesystem::path GetDataPath(bool checkForMods) { - return GetSavePath() / "ACH-DATA"; + return GetSavePath(checkForMods) / "ACH-DATA"; } static time_t GetTimestamp(uint16_t id); diff --git a/UnleashedRecomp/user/paths.h b/UnleashedRecomp/user/paths.h index 53a40e8..a081f85 100644 --- a/UnleashedRecomp/user/paths.h +++ b/UnleashedRecomp/user/paths.h @@ -1,5 +1,7 @@ #pragma once +#include + #define USER_DIRECTORY "SWA" #ifndef GAME_INSTALL_DIRECTORY @@ -48,7 +50,20 @@ inline std::filesystem::path GetUserPath() return userPath; } -inline std::filesystem::path GetSavePath() +inline std::filesystem::path GetSavePath(bool checkForMods) { - return GetUserPath() / "save"; + if (checkForMods && !ModLoader::s_saveFilePath.empty()) + return ModLoader::s_saveFilePath.parent_path(); + else + return GetUserPath() / "save"; +} + +// Returned file name may not necessarily be +// equal to SYS-DATA as mods can assign anything. +inline std::filesystem::path GetSaveFilePath(bool checkForMods) +{ + if (checkForMods && !ModLoader::s_saveFilePath.empty()) + return ModLoader::s_saveFilePath; + else + return GetSavePath(false) / "SYS-DATA"; }