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:
Skyth (Asilkan)
2024-12-31 20:20:07 +03:00
committed by GitHub
parent 281535ad51
commit a397a90551
14 changed files with 1195 additions and 83 deletions
+46
View 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
View 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
View 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
View 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();
};