mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-10-30 07:11:05 +00:00
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.
This commit is contained in:
parent
281535ad51
commit
a397a90551
14 changed files with 1195 additions and 83 deletions
|
|
@ -194,6 +194,10 @@ set(SWA_USER_CXX_SOURCES
|
||||||
"user/config.cpp"
|
"user/config.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(SWA_MOD_CXX_SOURCES
|
||||||
|
"mod/mod_loader.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
set(SWA_THIRDPARTY_SOURCES
|
set(SWA_THIRDPARTY_SOURCES
|
||||||
"${SWA_THIRDPARTY_ROOT}/imgui/backends/imgui_impl_sdl2.cpp"
|
"${SWA_THIRDPARTY_ROOT}/imgui/backends/imgui_impl_sdl2.cpp"
|
||||||
"${SWA_THIRDPARTY_ROOT}/imgui/imgui.cpp"
|
"${SWA_THIRDPARTY_ROOT}/imgui/imgui.cpp"
|
||||||
|
|
@ -252,6 +256,7 @@ set(SWA_CXX_SOURCES
|
||||||
${SWA_UI_CXX_SOURCES}
|
${SWA_UI_CXX_SOURCES}
|
||||||
${SWA_INSTALL_CXX_SOURCES}
|
${SWA_INSTALL_CXX_SOURCES}
|
||||||
${SWA_USER_CXX_SOURCES}
|
${SWA_USER_CXX_SOURCES}
|
||||||
|
${SWA_MOD_CXX_SOURCES}
|
||||||
${SWA_THIRDPARTY_SOURCES}
|
${SWA_THIRDPARTY_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <kernel/function.h>
|
#include <kernel/function.h>
|
||||||
#include <cpu/guest_thread.h>
|
#include <cpu/guest_thread.h>
|
||||||
#include <os/logger.h>
|
#include <os/logger.h>
|
||||||
|
#include <mod/mod_loader.h>
|
||||||
|
|
||||||
struct FileHandle : KernelObject
|
struct FileHandle : KernelObject
|
||||||
{
|
{
|
||||||
|
|
@ -15,21 +16,62 @@ struct FileHandle : KernelObject
|
||||||
struct FindHandle : KernelObject
|
struct FindHandle : KernelObject
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::path searchPath;
|
ankerl::unordered_dense::map<std::u8string, std::pair<size_t, bool>> searchResult; // Relative path, file size, is directory
|
||||||
std::filesystem::directory_iterator iterator;
|
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)
|
void fillFindData(WIN32_FIND_DATAA* lpFindFileData)
|
||||||
{
|
{
|
||||||
if (iterator->is_directory())
|
if (iterator->second.second)
|
||||||
lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_DIRECTORY);
|
lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_DIRECTORY);
|
||||||
else if (iterator->is_regular_file())
|
else
|
||||||
lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_NORMAL);
|
lpFindFileData->dwFileAttributes = ByteSwap(FILE_ATTRIBUTE_NORMAL);
|
||||||
|
|
||||||
std::u8string pathU8Str = iterator->path().lexically_relative(searchPath).u8string();
|
strncpy(lpFindFileData->cFileName, (const char *)(iterator->first.c_str()), sizeof(lpFindFileData->cFileName));
|
||||||
uint64_t fileSize = iterator->file_size(ec);
|
lpFindFileData->nFileSizeLow = ByteSwap(uint32_t(iterator->second.first >> 32U));
|
||||||
strncpy(lpFindFileData->cFileName, (const char *)(pathU8Str.c_str()), sizeof(lpFindFileData->cFileName));
|
lpFindFileData->nFileSizeHigh = ByteSwap(uint32_t(iterator->second.first));
|
||||||
lpFindFileData->nFileSizeLow = ByteSwap(uint32_t(fileSize >> 32U));
|
|
||||||
lpFindFileData->nFileSizeHigh = ByteSwap(uint32_t(fileSize));
|
|
||||||
lpFindFileData->ftCreationTime = {};
|
lpFindFileData->ftCreationTime = {};
|
||||||
lpFindFileData->ftLastAccessTime = {};
|
lpFindFileData->ftLastAccessTime = {};
|
||||||
lpFindFileData->ftLastWriteTime = {};
|
lpFindFileData->ftLastWriteTime = {};
|
||||||
|
|
@ -50,7 +92,7 @@ SWA_API FileHandle* XCreateFileA
|
||||||
assert(((dwShareMode & ~(FILE_SHARE_READ | FILE_SHARE_WRITE)) == 0) && "Unknown share mode bits.");
|
assert(((dwShareMode & ~(FILE_SHARE_READ | FILE_SHARE_WRITE)) == 0) && "Unknown share mode bits.");
|
||||||
assert(((dwCreationDisposition & ~(CREATE_NEW | CREATE_ALWAYS)) == 0) && "Unknown creation disposition 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::fstream fileStream;
|
||||||
std::ios::openmode fileOpenMode = std::ios::binary;
|
std::ios::openmode fileOpenMode = std::ios::binary;
|
||||||
if (dwDesiredAccess & (GENERIC_READ | FILE_READ_DATA))
|
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)
|
FindHandle* XFindFirstFileA(const char* lpFileName, WIN32_FIND_DATAA* lpFindFileData)
|
||||||
{
|
{
|
||||||
const char *transformedPath = FileSystem::TransformPath(lpFileName);
|
std::string_view path = lpFileName;
|
||||||
size_t transformedPathLength = strlen(transformedPath);
|
if (path.find("\\*") == (path.size() - 2) || path.find("/*") == (path.size() - 2))
|
||||||
if (transformedPathLength == 0)
|
|
||||||
return (FindHandle*)GUEST_INVALID_HANDLE_VALUE;
|
|
||||||
|
|
||||||
std::filesystem::path dirPath;
|
|
||||||
if (strstr(transformedPath, "\\*") == (&transformedPath[transformedPathLength - 2]) || strstr(transformedPath, "/*") == (&transformedPath[transformedPathLength - 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
|
else
|
||||||
{
|
{
|
||||||
dirPath = std::u8string_view((const char8_t *)(transformedPath), transformedPathLength);
|
assert(!std::filesystem::path(path).has_extension() && "Unknown search pattern.");
|
||||||
assert(!dirPath.has_extension() && "Unknown search pattern.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!std::filesystem::is_directory(dirPath))
|
FindHandle findHandle(path);
|
||||||
|
|
||||||
|
if (findHandle.searchResult.empty())
|
||||||
return GetInvalidKernelObject<FindHandle>();
|
return GetInvalidKernelObject<FindHandle>();
|
||||||
|
|
||||||
std::filesystem::directory_iterator dirIterator(dirPath);
|
findHandle.fillFindData(lpFindFileData);
|
||||||
if (dirIterator == std::filesystem::directory_iterator())
|
|
||||||
return GetInvalidKernelObject<FindHandle>();
|
|
||||||
|
|
||||||
FindHandle *findHandle = CreateKernelObject<FindHandle>();
|
return CreateKernelObject<FindHandle>(std::move(findHandle));
|
||||||
findHandle->searchPath = std::move(dirPath);
|
|
||||||
findHandle->iterator = std::move(dirIterator);
|
|
||||||
findHandle->fillFindData(lpFindFileData);
|
|
||||||
return findHandle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t XFindNextFileA(FindHandle* Handle, WIN32_FIND_DATAA* lpFindFileData)
|
uint32_t XFindNextFileA(FindHandle* Handle, WIN32_FIND_DATAA* lpFindFileData)
|
||||||
{
|
{
|
||||||
Handle->iterator++;
|
Handle->iterator++;
|
||||||
|
|
||||||
if (Handle->iterator == std::filesystem::directory_iterator())
|
if (Handle->iterator == Handle->searchResult.end())
|
||||||
{
|
{
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +337,7 @@ uint32_t XReadFileEx(FileHandle* hFile, void* lpBuffer, uint32_t nNumberOfBytesT
|
||||||
|
|
||||||
uint32_t XGetFileAttributesA(const char* lpFileName)
|
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))
|
if (std::filesystem::is_directory(filePath))
|
||||||
return FILE_ATTRIBUTE_DIRECTORY;
|
return FILE_ATTRIBUTE_DIRECTORY;
|
||||||
else if (std::filesystem::is_regular_file(filePath))
|
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;
|
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 == '\\')
|
std::filesystem::path resolvedPath = ModLoader::ResolvePath(path);
|
||||||
{
|
if (!resolvedPath.empty())
|
||||||
*path = '/';
|
return resolvedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
path++;
|
thread_local std::string builtPath;
|
||||||
}
|
builtPath.clear();
|
||||||
}
|
|
||||||
|
|
||||||
const char* FileSystem::TransformPath(const char* path)
|
size_t index = path.find(":\\");
|
||||||
{
|
if (index != std::string::npos)
|
||||||
thread_local char builtPath[2048]{};
|
|
||||||
const char* relativePath = strstr(path, ":\\");
|
|
||||||
if (relativePath != nullptr)
|
|
||||||
{
|
{
|
||||||
// rooted folder, handle direction
|
// 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);
|
const auto newRoot = XamGetRootPath(root);
|
||||||
|
|
||||||
if (!newRoot.empty())
|
if (!newRoot.empty())
|
||||||
{
|
{
|
||||||
strncpy(builtPath, newRoot.data(), newRoot.size());
|
builtPath += newRoot;
|
||||||
builtPath[newRoot.size()] = '\\';
|
builtPath += '/';
|
||||||
strcpy(builtPath + newRoot.size() + 1, relativePath + 2);
|
}
|
||||||
|
|
||||||
|
builtPath += path.substr(index + 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
strncpy(builtPath, relativePath + 2, sizeof(builtPath));
|
builtPath += path;
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
strncpy(builtPath, path, sizeof(builtPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fixSlashes(builtPath);
|
std::replace(builtPath.begin(), builtPath.end(), '\\', '/');
|
||||||
return builtPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
SWA_API const char* XExpandFilePathA(const char* path)
|
return std::u8string_view((const char8_t*)builtPath.c_str());
|
||||||
{
|
|
||||||
return FileSystem::TransformPath(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GUEST_FUNCTION_HOOK(sub_82BD4668, XCreateFileA);
|
GUEST_FUNCTION_HOOK(sub_82BD4668, XCreateFileA);
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
struct FileSystem
|
struct FileSystem
|
||||||
{
|
{
|
||||||
static const char* TransformPath(const char* path);
|
static std::filesystem::path ResolvePath(const std::string_view& path, bool checkForMods);
|
||||||
};
|
};
|
||||||
|
|
@ -19,6 +19,11 @@ public:
|
||||||
void* Commit(size_t offset, size_t size);
|
void* Commit(size_t offset, size_t size);
|
||||||
void* Reserve(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
|
void* Translate(size_t offset) const noexcept
|
||||||
{
|
{
|
||||||
if (offset)
|
if (offset)
|
||||||
|
|
@ -27,12 +32,12 @@ public:
|
||||||
return base + offset;
|
return base + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t MapVirtual(void* host) const noexcept
|
uint32_t MapVirtual(const void* host) const noexcept
|
||||||
{
|
{
|
||||||
if (host)
|
if (host)
|
||||||
assert(host >= base && host < (base + size));
|
assert(IsInMemoryRange(host));
|
||||||
|
|
||||||
return static_cast<uint32_t>(static_cast<char*>(host) - base);
|
return static_cast<uint32_t>(static_cast<const char*>(host) - base);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ SWA_API uint32_t XamContentCreateEx(uint32_t dwUserIndex, const char* szRootName
|
||||||
|
|
||||||
if (pContentData->dwContentType == XCONTENTTYPE_SAVEDATA)
|
if (pContentData->dwContentType == XCONTENTTYPE_SAVEDATA)
|
||||||
{
|
{
|
||||||
std::u8string savePathU8 = GetSavePath().u8string();
|
std::u8string savePathU8 = GetSavePath(true).u8string();
|
||||||
root = (const char *)(savePathU8.c_str());
|
root = (const char *)(savePathU8.c_str());
|
||||||
}
|
}
|
||||||
else if (pContentData->dwContentType == XCONTENTTYPE_DLC)
|
else if (pContentData->dwContentType == XCONTENTTYPE_DLC)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#include <install/installer.h>
|
#include <install/installer.h>
|
||||||
#include <os/logger.h>
|
#include <os/logger.h>
|
||||||
#include <ui/installer_wizard.h>
|
#include <ui/installer_wizard.h>
|
||||||
|
#include <mod/mod_loader.h>
|
||||||
|
|
||||||
#define GAME_XEX_PATH "game:\\default.xex"
|
#define GAME_XEX_PATH "game:\\default.xex"
|
||||||
|
|
||||||
|
|
@ -53,13 +54,26 @@ void KiSystemStartup()
|
||||||
XamRegisterContent(gameContent, GAME_INSTALL_DIRECTORY "/game");
|
XamRegisterContent(gameContent, GAME_INSTALL_DIRECTORY "/game");
|
||||||
XamRegisterContent(updateContent, GAME_INSTALL_DIRECTORY "/update");
|
XamRegisterContent(updateContent, GAME_INSTALL_DIRECTORY "/update");
|
||||||
|
|
||||||
const auto savePath = GetSavePath();
|
const auto saveFilePath = GetSaveFilePath(true);
|
||||||
const auto saveName = "SYS-DATA";
|
bool saveFileExists = std::filesystem::exists(saveFilePath);
|
||||||
|
|
||||||
if (std::filesystem::exists(savePath / saveName))
|
if (!saveFileExists)
|
||||||
{
|
{
|
||||||
std::u8string savePathU8 = savePath.u8string();
|
// Copy base save data to modded save as fallback.
|
||||||
XamRegisterContent(XamMakeContent(XCONTENTTYPE_SAVEDATA, saveName), (const char *)(savePathU8.c_str()));
|
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
|
// Mount game
|
||||||
|
|
@ -175,12 +189,14 @@ int main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModLoader::Init();
|
||||||
|
|
||||||
AchievementData::Load();
|
AchievementData::Load();
|
||||||
|
|
||||||
KiSystemStartup();
|
KiSystemStartup();
|
||||||
|
|
||||||
const char *modulePath = FileSystem::TransformPath(GAME_XEX_PATH);
|
auto modulePath = FileSystem::ResolvePath(GAME_XEX_PATH, false);
|
||||||
uint32_t entry = LdrLoadModule(std::u8string_view((const char8_t*)(modulePath)));
|
uint32_t entry = LdrLoadModule(modulePath);
|
||||||
|
|
||||||
if (!runInstallerWizard)
|
if (!runInstallerWizard)
|
||||||
Video::CreateHostDevice(sdlVideoDriver);
|
Video::CreateHostDevice(sdlVideoDriver);
|
||||||
|
|
|
||||||
46
UnleashedRecomp/mod/ini_file.h
Normal file
46
UnleashedRecomp/mod/ini_file.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <xxHashMap.h>
|
||||||
|
|
||||||
|
class IniFile
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
struct Property
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::string value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Section
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
xxHashMap<Property> properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
xxHashMap<Section> 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<typename T>
|
||||||
|
T get(const std::string_view& sectionName, const std::string_view& propertyName, T defaultValue) const;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void enumerate(const T& function) const;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void enumerate(const std::string_view& sectionName, const T& function) const;
|
||||||
|
|
||||||
|
bool contains(const std::string_view& sectionName) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#include "ini_file.inl"
|
||||||
205
UnleashedRecomp/mod/ini_file.inl
Normal file
205
UnleashedRecomp/mod/ini_file.inl
Normal file
|
|
@ -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<size_t>(file.tellg());
|
||||||
|
const auto data = std::make_unique<char[]>(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 <typename T>
|
||||||
|
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<typename T>
|
||||||
|
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 <typename T>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
781
UnleashedRecomp/mod/mod_loader.cpp
Normal file
781
UnleashedRecomp/mod/mod_loader.cpp
Normal file
|
|
@ -0,0 +1,781 @@
|
||||||
|
#include "mod_loader.h"
|
||||||
|
#include "ini_file.h"
|
||||||
|
|
||||||
|
#include <api/Hedgehog/Base/System/hhAllocator.h>
|
||||||
|
#include <cpu/guest_stack_var.h>
|
||||||
|
#include <kernel/function.h>
|
||||||
|
#include <kernel/heap.h>
|
||||||
|
#include <xxHashMap.h>
|
||||||
|
|
||||||
|
enum class ModType
|
||||||
|
{
|
||||||
|
HMM,
|
||||||
|
UMM
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mod
|
||||||
|
{
|
||||||
|
ModType type{};
|
||||||
|
std::vector<std::filesystem::path> includeDirs;
|
||||||
|
bool merge = false;
|
||||||
|
ankerl::unordered_dense::set<std::filesystem::path> readOnly;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<Mod> 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<std::filesystem::path> 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<std::filesystem::path>* 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<size_t>("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<size_t>("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<uint8_t> decompressLzx(PPCContext& ctx, uint8_t* base, const uint8_t* compressedData, size_t compressedDataSize, be<uint32_t>* scratchSpace)
|
||||||
|
{
|
||||||
|
assert(g_memory.IsInMemoryRange(compressedData));
|
||||||
|
|
||||||
|
bool shouldFreeScratchSpace = false;
|
||||||
|
if (scratchSpace == nullptr)
|
||||||
|
{
|
||||||
|
scratchSpace = reinterpret_cast<be<uint32_t>*>(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<const be<uint32_t>*>(compressedData + 0x8);
|
||||||
|
ctx.r6.u32 = uint32_t(reinterpret_cast<uint8_t*>(scratchSpace) - base);
|
||||||
|
sub_831CE1A0(ctx, base);
|
||||||
|
|
||||||
|
uint64_t decompressedDataSize = *reinterpret_cast<const be<uint64_t>*>(compressedData + 0x18);
|
||||||
|
uint8_t* decompressedData = reinterpret_cast<uint8_t*>(g_userHeap.Alloc(decompressedDataSize));
|
||||||
|
|
||||||
|
uint32_t blockSize = *reinterpret_cast<const be<uint32_t>*>(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<const be<uint32_t>*>(compressedData + compressedDataOffset);
|
||||||
|
|
||||||
|
// Decompress
|
||||||
|
ctx.r3.u32 = *scratchSpace;
|
||||||
|
ctx.r4.u32 = uint32_t((decompressedData + decompressedDataOffset) - base);
|
||||||
|
ctx.r5.u32 = uint32_t(reinterpret_cast<uint8_t*>(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<std::string> 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<const ArlHeader*>(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<const char*>(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<const ArEntry*>(arFileData + i);
|
||||||
|
s_fileNames.emplace(reinterpret_cast<const char*>(entry + 1));
|
||||||
|
i += entry->entrySize;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto r3 = ctx.r3;
|
||||||
|
auto r4 = ctx.r4;
|
||||||
|
auto r5 = ctx.r5;
|
||||||
|
auto r6 = ctx.r6;
|
||||||
|
|
||||||
|
auto loadFile = [&]<typename TFunction>(const std::filesystem::path& filePath, const TFunction& function)
|
||||||
|
{
|
||||||
|
std::ifstream stream(filePath, std::ios::binary);
|
||||||
|
if (stream.good())
|
||||||
|
{
|
||||||
|
be<uint32_t> signature{};
|
||||||
|
stream.read(reinterpret_cast<char*>(&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<char*>(compressedFileData), arlFileSize);
|
||||||
|
stream.close();
|
||||||
|
|
||||||
|
auto fileData = decompressLzx(ctx, base, reinterpret_cast<uint8_t*>(compressedFileData), arlFileSize, nullptr);
|
||||||
|
|
||||||
|
g_userHeap.Free(compressedFileData);
|
||||||
|
|
||||||
|
function(fileData.data(), fileData.size());
|
||||||
|
|
||||||
|
g_userHeap.Free(fileData.data());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
thread_local std::vector<uint8_t> s_fileData;
|
||||||
|
|
||||||
|
s_fileData.resize(arlFileSize);
|
||||||
|
stream.read(reinterpret_cast<char*>(s_fileData.data()), arlFileSize);
|
||||||
|
stream.close();
|
||||||
|
|
||||||
|
function(s_fileData.data(), arlFileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local xxHashMap<std::vector<std::pair<std::filesystem::path, bool>>> s_cache;
|
||||||
|
|
||||||
|
std::u8string_view arlFilePathU8(reinterpret_cast<const char8_t*>(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<std::pair<std::filesystem::path, bool>> 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<uint8_t*>(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<const char8_t*>(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<char*>(arFileData), arFileSize);
|
||||||
|
stream.close();
|
||||||
|
|
||||||
|
auto arFileDataHolder = reinterpret_cast<be<uint32_t>*>(g_userHeap.Alloc(sizeof(uint32_t) * 2));
|
||||||
|
|
||||||
|
if (*reinterpret_cast<be<uint32_t>*>(arFileData) == LZX_SIGNATURE)
|
||||||
|
{
|
||||||
|
auto fileData = decompressLzx(ctx, base, reinterpret_cast<uint8_t*>(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<std::vector<std::filesystem::path>> 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<std::filesystem::path> 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<uint32_t> signature{};
|
||||||
|
uint32_t splitCount{};
|
||||||
|
stream.read(reinterpret_cast<char*>(&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<char*>(compressedFileData), arlFileSize);
|
||||||
|
stream.close();
|
||||||
|
|
||||||
|
auto fileData = decompressLzx(ctx, base, reinterpret_cast<uint8_t*>(compressedFileData), arlFileSize, nullptr);
|
||||||
|
|
||||||
|
g_userHeap.Free(compressedFileData);
|
||||||
|
|
||||||
|
splitCount = *reinterpret_cast<uint32_t*>(fileData.data() + 0x4);
|
||||||
|
|
||||||
|
g_userHeap.Free(fileData.data());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stream.read(reinterpret_cast<char*>(&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);
|
||||||
|
}
|
||||||
12
UnleashedRecomp/mod/mod_loader.h
Normal file
12
UnleashedRecomp/mod/mod_loader.h
Normal file
|
|
@ -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<std::filesystem::path>* GetIncludeDirectories(size_t modIndex);
|
||||||
|
|
||||||
|
static void Init();
|
||||||
|
};
|
||||||
|
|
@ -49,6 +49,7 @@ using Microsoft::WRL::ComPtr;
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <semaphore>
|
#include <semaphore>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <charconv>
|
||||||
|
|
||||||
#include "framework.h"
|
#include "framework.h"
|
||||||
#include "mutex.h"
|
#include "mutex.h"
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,15 @@ bool AchievementData::VerifyChecksum()
|
||||||
|
|
||||||
void AchievementData::Load()
|
void AchievementData::Load()
|
||||||
{
|
{
|
||||||
auto dataPath = GetDataPath();
|
auto dataPath = GetDataPath(true);
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(dataPath))
|
||||||
|
{
|
||||||
|
// Try loading base achievement data as fallback.
|
||||||
|
dataPath = GetDataPath(false);
|
||||||
if (!std::filesystem::exists(dataPath))
|
if (!std::filesystem::exists(dataPath))
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::ifstream file(dataPath, std::ios::binary);
|
std::ifstream file(dataPath, std::ios::binary);
|
||||||
|
|
||||||
|
|
@ -160,7 +165,7 @@ void AchievementData::Load()
|
||||||
|
|
||||||
void AchievementData::Save()
|
void AchievementData::Save()
|
||||||
{
|
{
|
||||||
std::ofstream file(GetDataPath(), std::ios::binary);
|
std::ofstream file(GetDataPath(true), std::ios::binary);
|
||||||
|
|
||||||
if (!file)
|
if (!file)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ public:
|
||||||
|
|
||||||
static inline Data Data{ ACH_SIGNATURE, ACH_VERSION };
|
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);
|
static time_t GetTimestamp(uint16_t id);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <mod/mod_loader.h>
|
||||||
|
|
||||||
#define USER_DIRECTORY "SWA"
|
#define USER_DIRECTORY "SWA"
|
||||||
|
|
||||||
#ifndef GAME_INSTALL_DIRECTORY
|
#ifndef GAME_INSTALL_DIRECTORY
|
||||||
|
|
@ -48,7 +50,20 @@ inline std::filesystem::path GetUserPath()
|
||||||
return userPath;
|
return userPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::filesystem::path GetSavePath()
|
inline std::filesystem::path GetSavePath(bool checkForMods)
|
||||||
{
|
{
|
||||||
|
if (checkForMods && !ModLoader::s_saveFilePath.empty())
|
||||||
|
return ModLoader::s_saveFilePath.parent_path();
|
||||||
|
else
|
||||||
return GetUserPath() / "save";
|
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";
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue