From 4d731ba32bff0a45a84e9a02120bc27b9dd522a3 Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sat, 9 May 2026 20:51:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B4=B4=E5=9B=BE?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resource/config.json | 5 +- resource/localizationConfig.json | 4 +- src/GakumasLocalify/Hook.cpp | 857 +++++++++++++++++++++++- src/GakumasLocalify/config/Config.cpp | 6 + src/GakumasLocalify/config/Config.hpp | 2 + src/gkmsGUI/GUII18n.cpp | 6 +- src/gkmsGUI/gkmsGUILoop.cpp | 21 + src/gkmsGUI/i18nData/strings_en.hpp | 9 +- src/gkmsGUI/i18nData/strings_ja.hpp | 9 +- src/gkmsGUI/i18nData/strings_zh-rCN.hpp | 9 +- src/gkmsGUI/i18nData/strings_zh-rTW.hpp | 124 ++++ src/main.cpp | 17 +- src/platformDefine.hpp | 2 +- src/resourceUpdate/resourceUpdate.cpp | 311 ++++++++- src/resourceUpdate/resourceUpdate.hpp | 6 +- src/stdinclude.hpp | 9 +- src/windowsPlatform.cpp | 17 +- 17 files changed, 1360 insertions(+), 54 deletions(-) create mode 100644 src/gkmsGUI/i18nData/strings_zh-rTW.hpp diff --git a/resource/config.json b/resource/config.json index 347659f..63590d7 100644 --- a/resource/config.json +++ b/resource/config.json @@ -5,7 +5,10 @@ "useRemoteAssets": true, "useAPIAssets": false, "useAPIAssetsURL": "https://uma.chinosk6.cn/api/gkms_trans_data", + "useAPITextureAssets": false, + "useAPITextureAssetsURL": "https://texture.gakumas.cn/api/gkms_texture_data", + "delTextureRemoteAfterUpdate": true, "delRemoteAfterUpdate": false, "cleanLocalAssets": false, "p": false -} \ No newline at end of file +} diff --git a/resource/localizationConfig.json b/resource/localizationConfig.json index 5d38ba7..a8de5cd 100644 --- a/resource/localizationConfig.json +++ b/resource/localizationConfig.json @@ -3,9 +3,11 @@ "enabled": true, "lazyInit": true, "replaceFont": true, + "replaceTexture": true, "textTest": false, "useMasterTrans": true, "dumpText": false, + "dumpRuntimeTexture": false, "gameOrientation": 0, "forceExportResource": false, "enableFreeCamera": false, @@ -42,4 +44,4 @@ "bLimitZx": 1.5, "bLimitZy": 1.5, "pf": false -} \ No newline at end of file +} diff --git a/src/GakumasLocalify/Hook.cpp b/src/GakumasLocalify/Hook.cpp index dd5290e..b197d67 100644 --- a/src/GakumasLocalify/Hook.cpp +++ b/src/GakumasLocalify/Hook.cpp @@ -13,6 +13,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "../platformDefine.hpp" #ifdef GKMS_WINDOWS @@ -262,10 +267,27 @@ namespace GakumasLocal::HookMain { std::unordered_map loadHistory{}; + void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName); + void* ReplaceTextureOrSpriteByObjectName(void* result); + void ReplaceAllAssetTextures(void* allAssets); + void* ReplaceSpriteAssetByTextureName(void* sprite); + void* ReplaceSpriteTexture(void* texture2D); + void DumpTextureOrSpriteAsset(void* result); + + DEFINE_HOOK(void*, AssetBundle_LoadAsset, (void* self, Il2cppString* name, void* type)) { + auto result = AssetBundle_LoadAsset_Orig(self, name, type); + if (name) { + result = ReplaceTextureOrSpriteAsset(result, name->ToString()); + } + return result; + } + DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) { // Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str()); auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type); - loadHistory.emplace(ret, name->ToString()); + if (ret && name) { + loadHistory.emplace(ret, name->ToString()); + } return ret; } @@ -277,18 +299,62 @@ namespace GakumasLocal::HookMain { // const auto assetClass = Il2cppUtils::get_class_from_instance(result); // Log::InfoFmt("AssetBundleRequest_GetResult: %s, type: %s", name.c_str(), static_cast(assetClass)->name); + result = ReplaceTextureOrSpriteAsset(result, name); } return result; } + DEFINE_HOOK(void*, AssetBundleRequest_get_asset, (void* self)) { + std::string name; + if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) { + name = iter->second; + loadHistory.erase(iter); + } + + auto result = AssetBundleRequest_get_asset_Orig(self); + if (!name.empty()) { + result = ReplaceTextureOrSpriteAsset(result, name); + } + return result; + } + + DEFINE_HOOK(void*, AssetBundleRequest_get_allAssets, (void* self)) { + auto result = AssetBundleRequest_get_allAssets_Orig(self); + ReplaceAllAssetTextures(result); + return result; + } + DEFINE_HOOK(void*, Resources_Load, (Il2cppString* path, void* systemTypeInstance)) { auto ret = Resources_Load_Orig(path, systemTypeInstance); // if (ret) Log::DebugFmt("Resources_Load: %s, type: %s", path->ToString().c_str(), Il2cppUtils::get_class_from_instance(ret)->name); + if (path) { + ret = ReplaceTextureOrSpriteAsset(ret, path->ToString()); + } return ret; } + DEFINE_HOOK(void*, Sprite_get_texture, (void* self)) { + return ReplaceSpriteTexture(Sprite_get_texture_Orig(self)); + } + + DEFINE_HOOK(void, Image_set_sprite, (void* self, void* sprite)) { + Image_set_sprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite)); + } + + DEFINE_HOOK(void, Image_set_overrideSprite, (void* self, void* sprite)) { + Image_set_overrideSprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite)); + } + + DEFINE_HOOK(void, CanvasRenderer_SetTexture, (void* self, void* texture)) { + CanvasRenderer_SetTexture_Orig(self, ReplaceTextureOrSpriteByObjectName(texture)); + } + + DEFINE_HOOK(void, SpriteRenderer_set_sprite, (void* self, void* sprite)) { + SpriteRenderer_set_sprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite)); + } + DEFINE_HOOK(void, I18nHelper_SetUpI18n, (void* self, Il2cppString* lang, Il2cppString* localizationText, int keyComparison)) { // Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str()); // TODO 此处为 dump 原文 csv @@ -460,6 +526,775 @@ namespace GakumasLocal::HookMain { } #endif + Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr; + Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr; + std::unordered_map LoadedLocalTextureHandles{}; + std::unordered_set AppliedLocalTextureKeys{}; + + Il2cppUtils::Il2CppClassHead* GetTexture2DClass() { + if (!Texture2DClass) { + const auto textureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Texture2D"); + if (textureClass) { + Texture2DClass = static_cast(textureClass->address); + } + } + return Texture2DClass; + } + + Il2cppUtils::Il2CppClassHead* GetSpriteClass() { + if (!SpriteClass) { + const auto spriteClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite"); + if (spriteClass) { + SpriteClass = static_cast(spriteClass->address); + } + } + return SpriteClass; + } + + bool IsTexture2D(void* obj) { + const auto textureClass = GetTexture2DClass(); + if (!obj || !textureClass) return false; + + const auto objClass = Il2cppUtils::get_class_from_instance(obj); + if (objClass == textureClass) return true; + + return UnityResolve::Invoke("il2cpp_class_is_assignable_from", textureClass, objClass); + } + + bool IsSprite(void* obj) { + const auto spriteClass = GetSpriteClass(); + if (!obj || !spriteClass) return false; + + const auto objClass = Il2cppUtils::get_class_from_instance(obj); + if (objClass == spriteClass) return true; + + return UnityResolve::Invoke("il2cpp_class_is_assignable_from", spriteClass, objClass); + } + + Il2cppString* GetObjectName(void* obj) { + if (!obj) return nullptr; + + static auto Object_GetName = reinterpret_cast( + Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)")); + return Object_GetName ? Object_GetName(obj) : nullptr; + } + + void SetDontUnloadUnusedAsset(void* obj) { + if (!obj) return; + + static auto Object_set_hideFlags = reinterpret_cast( + Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Object", "set_hideFlags")); + if (Object_set_hideFlags) { + Object_set_hideFlags(obj, 32); + } + } + + void AddTexturePathCandidate(std::vector& candidates, const std::filesystem::path& path) { + if (path.empty()) return; + if (std::find(candidates.begin(), candidates.end(), path) == candidates.end()) { + candidates.emplace_back(path); + } + if (!path.has_extension()) { + auto pngPath = path; + pngPath += ".png"; + if (std::find(candidates.begin(), candidates.end(), pngPath) == candidates.end()) { + candidates.emplace_back(std::move(pngPath)); + } + } + } + + enum class TextureCategory { + Image, + Atlas, + Others, + }; + + std::string ToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; + } + + TextureCategory GetTextureCategory(const std::string& textureName) { + const auto lowerName = ToLowerAscii(std::filesystem::path(textureName).filename().generic_string()); + if (lowerName.rfind("img", 0) == 0) { + return TextureCategory::Image; + } + if (lowerName.rfind("sactx", 0) == 0) { + return TextureCategory::Atlas; + } + return TextureCategory::Others; + } + + std::filesystem::path GetTextureCategoryDirName(TextureCategory category) { + switch (category) { + case TextureCategory::Image: + return "image"; + case TextureCategory::Atlas: + return "atlas"; + default: + return "others"; + } + } + + std::filesystem::path GetTextureReplaceRoot() { + return Local::GetBasePath() / "texture2d"; + } + + std::filesystem::path GetTextureDumpRoot() { + return Local::GetBasePath() / "dump-files" / "texture2d"; + } + + std::filesystem::path GetTextureReplaceBase(const std::string& textureName) { + return GetTextureReplaceRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); + } + + std::filesystem::path GetTextureDumpBase(const std::string& textureName) { + return GetTextureDumpRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); + } + + std::vector SplitString(const std::string& value, char delimiter) { + std::vector parts; + size_t start = 0; + while (start <= value.size()) { + const auto end = value.find(delimiter, start); + parts.emplace_back(value.substr(start, end == std::string::npos ? std::string::npos : end - start)); + if (end == std::string::npos) break; + start = end + 1; + } + return parts; + } + + void AppendTextureCandidates(std::vector& target, std::vector&& source); + std::string NormalizeLocalAssetKey(const std::filesystem::path& path); + + bool IsHexHashPart(const std::string& value) { + return value.size() == 8 && std::all_of(value.begin(), value.end(), [](unsigned char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); + }); + } + + std::string GetPortableSactxTextureName(const std::string& objectName) { + auto fileName = std::filesystem::path(objectName).filename().generic_string(); + if (fileName.ends_with(".png")) { + fileName.resize(fileName.size() - 4); + } + + const auto parts = SplitString(fileName, '-'); + if (parts.size() < 5 || parts[0] != "sactx" || parts[2].find('x') == std::string::npos) { + return {}; + } + + const auto atlasEnd = IsHexHashPart(parts.back()) ? parts.size() - 1 : parts.size(); + if (atlasEnd <= 4) return {}; + + std::string portableName = parts[0] + "-" + parts[1] + "-" + parts[2]; + for (size_t i = 4; i < atlasEnd; ++i) { + portableName += "-" + parts[i]; + } + return portableName; + } + + std::unordered_map>> RecursiveTexturePathIndex{}; + + std::vector GetRecursiveTextureCandidates(const std::filesystem::path& basePath, + const std::string& lookupName) { + std::vector candidates; + if (lookupName.empty() || !std::filesystem::exists(basePath)) return candidates; + + const auto baseKey = NormalizeLocalAssetKey(basePath); + auto& index = RecursiveTexturePathIndex[baseKey]; + if (index.empty()) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(basePath)) { + if (!entry.is_regular_file()) continue; + + const auto& path = entry.path(); + if (ToLowerAscii(path.extension().generic_string()) != ".png") continue; + + const auto fileName = path.filename().generic_string(); + const auto stemName = path.stem().generic_string(); + index[fileName].emplace_back(path); + if (stemName != fileName) { + index[stemName].emplace_back(path); + } + } + } + + if (const auto iter = index.find(lookupName); iter != index.end()) { + candidates.insert(candidates.end(), iter->second.begin(), iter->second.end()); + } + return candidates; + } + + std::vector GetNamedTextureCandidates(const std::filesystem::path& assetName) { + std::vector candidates; + if (assetName.empty()) return candidates; + + const auto basePath = GetTextureReplaceBase(assetName.filename().generic_string()); + AddTexturePathCandidate(candidates, basePath / assetName); + if (assetName.has_parent_path()) { + AddTexturePathCandidate(candidates, basePath / assetName.filename()); + } + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, assetName.filename().generic_string())); + + const auto portableAssetName = GetPortableSactxTextureName(assetName.filename().generic_string()); + if (!portableAssetName.empty()) { + AddTexturePathCandidate(candidates, basePath / portableAssetName); + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableAssetName)); + } + return candidates; + } + + std::vector GetSpriteTextureCandidates(const std::string& objectName) { + std::vector candidates; + if (objectName.empty()) return candidates; + + auto safeObjectName = objectName; + std::replace(safeObjectName.begin(), safeObjectName.end(), '|', '_'); + + const auto basePath = GetTextureReplaceBase(safeObjectName); + AddTexturePathCandidate(candidates, basePath / objectName); + if (safeObjectName != objectName) { + AddTexturePathCandidate(candidates, basePath / safeObjectName); + } + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, safeObjectName)); + const auto portableObjectName = GetPortableSactxTextureName(safeObjectName); + if (!portableObjectName.empty() && portableObjectName != objectName && portableObjectName != safeObjectName) { + AddTexturePathCandidate(candidates, basePath / portableObjectName); + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableObjectName)); + } + return candidates; + } + + void AppendTextureCandidates(std::vector& target, std::vector&& source) { + target.insert(target.end(), + std::make_move_iterator(source.begin()), + std::make_move_iterator(source.end())); + } + + std::vector GetSpriteAssetTextureCandidates(void* sprite, const std::string& assetName) { + std::vector candidates; + + const auto assetPath = std::filesystem::path(assetName); + if (!assetName.empty()) { + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(assetPath.filename().generic_string())); + } + + if (sprite && Sprite_get_texture_Orig) { + if (const auto texture = Sprite_get_texture_Orig(sprite)) { + if (const auto textureName = GetObjectName(texture)) { + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(textureName->ToString())); + } + } + } + return candidates; + } + + std::string NormalizeLocalAssetKey(const std::filesystem::path& path) { + auto key = path.lexically_normal().generic_string(); + std::replace(key.begin(), key.end(), '\\', '/'); + return key; + } + + std::string SanitizeDumpPathPart(std::string part) { + constexpr std::string_view invalidChars = "<>:\"/\\|?*"; + if (part.empty() || part == "." || part == "..") return "_"; + + for (auto& ch : part) { + if (static_cast(ch) < 32 || invalidChars.find(ch) != std::string_view::npos) { + ch = '_'; + } + } + + while (!part.empty() && (part.back() == '.' || part.back() == ' ')) { + part.back() = '_'; + } + return part.empty() ? "_" : part; + } + + std::filesystem::path SanitizeDumpSubPath(const std::filesystem::path& dumpSubDir) { + std::filesystem::path safePath; + for (const auto& part : dumpSubDir) { + const auto partString = part.generic_string(); + if (partString.empty() || partString == "." || partString == ".." + || part == part.root_name() || part == part.root_directory()) { + continue; + } + safePath /= SanitizeDumpPathPart(partString); + } + return safePath; + } + + bool DumpTexture2D(void* texture2D) { + if (!IsTexture2D(texture2D)) return false; + + const auto objectName = GetObjectName(texture2D); + const auto textureName = objectName ? objectName->ToString() : std::string("texture"); + const auto dumpDir = GetTextureDumpBase(textureName); + const auto dumpPath = dumpDir / (SanitizeDumpPathPart(textureName) + ".png"); + + if (std::filesystem::exists(dumpPath)) return true; + + static auto Texture2D_get_width = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_width", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_get_height = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_height", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_ctor = [] { + const auto textureClass = GetTexture2DClass(); + const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; + return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; + }(); + static auto Texture2D_ReadPixels = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "ReadPixels", 3) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_Apply = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "Apply", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_GetTemporary = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "GetTemporary", 3) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_ReleaseTemporary = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "ReleaseTemporary", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_get_active = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "get_active", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_set_active = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "set_active", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Graphics_Blit = [] { + const auto graphicsClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Graphics"); + const auto method = graphicsClass ? Il2cppUtils::il2cpp_class_get_method_from_name(graphicsClass->address, "Blit", 2) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto ImageConversion_EncodeToPNG = [] { + using EncodeToPNGFn = void* (*)(void*); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.ImageConversion::EncodeToPNG(UnityEngine.Texture2D)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "EncodeToPNG", 1) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_WriteAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "WriteAllBytes", 2) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!Texture2D_get_width || !Texture2D_get_height || !Texture2D_ctor || !Texture2D_ReadPixels + || !Texture2D_Apply || !RenderTexture_GetTemporary || !RenderTexture_ReleaseTemporary + || !RenderTexture_get_active || !RenderTexture_set_active || !Graphics_Blit + || !ImageConversion_EncodeToPNG || !File_WriteAllBytes) { + Log::Error("DumpTexture2D failed: Unity texture dump API not found."); + return false; + } + + const auto width = Texture2D_get_width(texture2D); + const auto height = Texture2D_get_height(texture2D); + if (width <= 0 || height <= 0) return false; + + void* renderTexture = nullptr; + void* readableTexture = nullptr; + void* previousActive = nullptr; + const auto cleanup = [&] { + if (RenderTexture_get_active && RenderTexture_set_active + && (previousActive || RenderTexture_get_active() == renderTexture)) { + RenderTexture_set_active(previousActive); + } + if (renderTexture && RenderTexture_ReleaseTemporary) { + RenderTexture_ReleaseTemporary(renderTexture); + } + }; + + try { + std::filesystem::create_directories(dumpDir); + + renderTexture = RenderTexture_GetTemporary(width, height, 0); + if (!renderTexture) { + cleanup(); + return false; + } + + Graphics_Blit(texture2D, renderTexture); + previousActive = RenderTexture_get_active(); + RenderTexture_set_active(renderTexture); + + readableTexture = UnityResolve::Invoke("il2cpp_object_new", GetTexture2DClass()); + if (!readableTexture) { + cleanup(); + return false; + } + + Texture2D_ctor(readableTexture, width, height); + Texture2D_ReadPixels(readableTexture, UnityResolve::UnityType::Rect(0, 0, static_cast(width), static_cast(height)), 0, 0); + Texture2D_Apply(readableTexture); + + const auto pngBytes = ImageConversion_EncodeToPNG(readableTexture); + if (!pngBytes) { + cleanup(); + return false; + } + + File_WriteAllBytes(Il2cppString::New(dumpPath.string()), pngBytes); + Log::InfoFmt("Texture dumped: %s", dumpPath.string().c_str()); + cleanup(); + return true; + } + catch (const std::exception& ex) { + cleanup(); + Log::ErrorFmt("DumpTexture2D failed: %s", ex.what()); + return false; + } + catch (...) { + cleanup(); + Log::Error("DumpTexture2D failed: unknown error."); + return false; + } + } + + void DumpTextureOrSpriteAsset(void* result) { + if (!result) return; + + if (IsTexture2D(result)) { + DumpTexture2D(result); + return; + } + if (IsSprite(result) && Sprite_get_texture_Orig) { + if (const auto texture = Sprite_get_texture_Orig(result)) { + DumpTexture2D(texture); + } + } + } + + void* LoadLocalTexture2D(const std::filesystem::path& path) { + if (!std::filesystem::is_regular_file(path)) return nullptr; + + const auto cacheKey = NormalizeLocalAssetKey(path); + if (const auto iter = LoadedLocalTextureHandles.find(cacheKey); iter != LoadedLocalTextureHandles.end()) { + const auto cachedTexture = UnityResolve::Invoke("il2cpp_gchandle_get_target", iter->second); + if (cachedTexture && IsNativeObjectAlive(cachedTexture)) { + return cachedTexture; + } + + UnityResolve::Invoke("il2cpp_gchandle_free", iter->second); + LoadedLocalTextureHandles.erase(iter); + } + + const auto textureClass = GetTexture2DClass(); + if (!textureClass) return nullptr; + + static auto Texture2D_ctor = [] { + const auto textureClass = GetTexture2DClass(); + const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; + return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; + }(); + static auto ImageConversion_LoadImage = [] { + using LoadImageFn = bool (*)(void*, void*, bool); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_ReadAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) { + Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found."); + return nullptr; + } + + const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); + if (!fileBytes) return nullptr; + + const auto texture = UnityResolve::Invoke("il2cpp_object_new", textureClass); + Texture2D_ctor(texture, 2, 2); + if (!ImageConversion_LoadImage(texture, fileBytes, false)) { + Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str()); + return nullptr; + } + + SetDontUnloadUnusedAsset(texture); + LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke("il2cpp_gchandle_new", texture, false)); + Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str()); + return texture; + } + + void* LoadLocalTexture2DFromCandidates(const std::vector& candidates) { + for (const auto& candidate : candidates) { + if (auto texture = LoadLocalTexture2D(candidate)) { + return texture; + } + } + return nullptr; + } + + bool ApplyLocalImageToTexture2D(void* texture2D, const std::filesystem::path& path) { + if (!IsTexture2D(texture2D) || !std::filesystem::is_regular_file(path)) return false; + + auto cacheKey = NormalizeLocalAssetKey(path) + + "|" + std::to_string(reinterpret_cast(texture2D)); + if (AppliedLocalTextureKeys.contains(cacheKey)) return true; + + static auto ImageConversion_LoadImage = [] { + using LoadImageFn = bool (*)(void*, void*, bool); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_ReadAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!ImageConversion_LoadImage || !File_ReadAllBytes) { + Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found."); + return false; + } + + const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); + if (!fileBytes) return false; + + if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) { + Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str()); + return false; + } + + SetDontUnloadUnusedAsset(texture2D); + AppliedLocalTextureKeys.emplace(std::move(cacheKey)); + Log::InfoFmt("Texture replaced in-place from local file: %s", path.string().c_str()); + return true; + } + + bool ApplyLocalImageToTexture2DFromCandidates(void* texture2D, const std::vector& candidates) { + for (const auto& candidate : candidates) { + if (ApplyLocalImageToTexture2D(texture2D, candidate)) { + return true; + } + } + return false; + } + + bool ReplaceSpriteTextureInPlace(void* sprite, const std::vector& candidates) { + if (!sprite || !Sprite_get_texture_Orig) return false; + + const auto texture = Sprite_get_texture_Orig(sprite); + if (!IsTexture2D(texture)) return false; + + return ApplyLocalImageToTexture2DFromCandidates(texture, candidates); + } + + void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName) { + if (!Config::replaceTexture && !Config::dumpRuntimeTexture) return result; + + if (Config::dumpRuntimeTexture) { + DumpTextureOrSpriteAsset(result); + } + if (!Config::replaceTexture) return result; + + if (IsSprite(result)) { + if (ReplaceSpriteTextureInPlace(result, GetSpriteAssetTextureCandidates(result, assetName))) { + return result; + } + return result; + } + + if (result && !IsTexture2D(result)) return result; + + if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(std::filesystem::path(assetName)))) { + return localTexture; + } + return result; + } + + void* ReplaceTextureOrSpriteByObjectName(void* result) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !result) return result; + + const auto objectName = GetObjectName(result); + if (!objectName) return result; + + const auto assetPath = std::filesystem::path(objectName->ToString()); + if (Config::dumpRuntimeTexture) { + DumpTextureOrSpriteAsset(result); + } + if (!Config::replaceTexture) return result; + + if (IsSprite(result)) { + std::vector candidates; + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(objectName->ToString())); + if (ReplaceSpriteTextureInPlace(result, candidates)) { + return result; + } + return result; + } + + if (IsTexture2D(result)) { + if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(assetPath))) { + return localTexture; + } + } + + return result; + } + + void ReplaceAllAssetTextures(void* allAssets) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !allAssets) return; + + auto assets = reinterpret_cast*>(allAssets); + for (std::uintptr_t i = 0; i < assets->max_length; ++i) { + auto asset = assets->At(static_cast(i)); + auto replacedAsset = ReplaceTextureOrSpriteByObjectName(asset); + if (replacedAsset != asset) { + assets->At(static_cast(i)) = replacedAsset; + } + } + } + + void* ReplaceSpriteAssetByTextureName(void* sprite) { + if (!Config::replaceTexture || !sprite) return sprite; + + if (!IsSprite(sprite)) { + return sprite; + } + + if (ReplaceSpriteTextureInPlace(sprite, GetSpriteAssetTextureCandidates(sprite, ""))) { + return sprite; + } + return sprite; + } + + void* ReplaceSpriteTexture(void* texture2D) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !IsTexture2D(texture2D)) return texture2D; + + const auto objectName = GetObjectName(texture2D); + if (!objectName) return texture2D; + + if (Config::dumpRuntimeTexture) { + DumpTexture2D(texture2D); + } + if (!Config::replaceTexture) return texture2D; + + if (ApplyLocalImageToTexture2DFromCandidates(texture2D, GetSpriteTextureCandidates(objectName->ToString()))) { + return texture2D; + } + return texture2D; + } + + void* ResolveSpriteGetTextureHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) { + return addr; + } + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite", "get_texture"); + } + + void* ResolveAssetBundleLoadAssetHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", + "LoadAsset_Internal", {"System.String", "System.Type"}); + } + + void* ResolveAssetBundleLoadAssetAsyncHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", + "LoadAssetAsync_Internal", {"System.String", "System.Type"}); + } + + void* ResolveAssetBundleRequestResultHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::GetResult()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "GetResult"); + } + + void* ResolveAssetBundleRequestAssetHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_asset()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_asset"); + } + + void* ResolveAssetBundleRequestAllAssetsHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_allAssets()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_allAssets"); + } + + void* ResolveResourcesLoadHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "ResourcesAPIInternal", + "Load", {"System.String", "System.Type"}); + } + std::unordered_set updatedFontPtrs{}; void UpdateFont(void* TMP_Textself) { if (!Config::replaceFont) return; @@ -1688,12 +2523,18 @@ namespace GakumasLocal::HookMain { UnityResolve::Mode::Il2Cpp, Config::lazyInit); #endif - ADD_HOOK(AssetBundle_LoadAssetAsync, Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")); - ADD_HOOK(AssetBundleRequest_GetResult, Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.AssetBundleRequest::GetResult()")); - ADD_HOOK(Resources_Load, Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")); + // Temporarily isolate texture replacement to CanvasRenderer.SetTexture only. + // ADD_HOOK(AssetBundle_LoadAsset, ResolveAssetBundleLoadAssetHookAddress()); + // ADD_HOOK(AssetBundle_LoadAssetAsync, ResolveAssetBundleLoadAssetAsyncHookAddress()); + // ADD_HOOK(AssetBundleRequest_GetResult, ResolveAssetBundleRequestResultHookAddress()); + // ADD_HOOK(AssetBundleRequest_get_asset, ResolveAssetBundleRequestAssetHookAddress()); + // ADD_HOOK(AssetBundleRequest_get_allAssets, ResolveAssetBundleRequestAllAssetsHookAddress()); + // ADD_HOOK(Resources_Load, ResolveResourcesLoadHookAddress()); + // ADD_HOOK(Sprite_get_texture, ResolveSpriteGetTextureHookAddress()); + // ADD_HOOK(Image_set_sprite, Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI", "Image", "set_sprite")); + // ADD_HOOK(Image_set_overrideSprite, Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI", "Image", "set_overrideSprite")); + ADD_HOOK(CanvasRenderer_SetTexture, Il2cppUtils::GetMethodPointer("UnityEngine.UIModule.dll", "UnityEngine", "CanvasRenderer", "SetTexture", {"UnityEngine.Texture"})); + // ADD_HOOK(SpriteRenderer_set_sprite, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "SpriteRenderer", "set_sprite")); ADD_HOOK(I18nHelper_SetUpI18n, Il2cppUtils::GetMethodPointer("quaunity-ui.Runtime.dll", "Qua.UI", "I18nHelper", "SetUpI18n")); @@ -1987,7 +2828,7 @@ namespace GakumasLocal::HookMain { UnityResolveProgress::startInit = true; UnityResolveProgress::assembliesProgress.total = 2; UnityResolveProgress::assembliesProgress.current = 1; - UnityResolveProgress::classProgress.total = 36; + UnityResolveProgress::classProgress.total = 43; UnityResolveProgress::classProgress.current = 0; } diff --git a/src/GakumasLocalify/config/Config.cpp b/src/GakumasLocalify/config/Config.cpp index 57ea8e3..f1bacea 100644 --- a/src/GakumasLocalify/config/Config.cpp +++ b/src/GakumasLocalify/config/Config.cpp @@ -11,11 +11,13 @@ namespace GakumasLocal::Config { bool enabled = true; bool lazyInit = true; bool replaceFont = true; + bool replaceTexture = true; bool forceExportResource = true; bool textTest = false; bool useMasterTrans = true; int gameOrientation = 0; bool dumpText = false; + bool dumpRuntimeTexture = false; bool enableFreeCamera = false; int targetFrameRate = 0; bool unlockAllLive = false; @@ -66,11 +68,13 @@ namespace GakumasLocal::Config { GetConfigItem(enabled); GetConfigItem(lazyInit); GetConfigItem(replaceFont); + GetConfigItem(replaceTexture); GetConfigItem(forceExportResource); GetConfigItem(gameOrientation); GetConfigItem(textTest); GetConfigItem(useMasterTrans); GetConfigItem(dumpText); + GetConfigItem(dumpRuntimeTexture); GetConfigItem(targetFrameRate); GetConfigItem(enableFreeCamera); GetConfigItem(unlockAllLive); @@ -122,11 +126,13 @@ namespace GakumasLocal::Config { SetConfigItem(enabled); SetConfigItem(lazyInit); SetConfigItem(replaceFont); + SetConfigItem(replaceTexture); SetConfigItem(forceExportResource); SetConfigItem(gameOrientation); SetConfigItem(textTest); SetConfigItem(useMasterTrans); SetConfigItem(dumpText); + SetConfigItem(dumpRuntimeTexture); SetConfigItem(targetFrameRate); SetConfigItem(enableFreeCamera); SetConfigItem(unlockAllLive); diff --git a/src/GakumasLocalify/config/Config.hpp b/src/GakumasLocalify/config/Config.hpp index 1147f53..348f0e3 100644 --- a/src/GakumasLocalify/config/Config.hpp +++ b/src/GakumasLocalify/config/Config.hpp @@ -7,11 +7,13 @@ namespace GakumasLocal::Config { extern bool enabled; extern bool lazyInit; extern bool replaceFont; + extern bool replaceTexture; extern bool forceExportResource; extern int gameOrientation; extern bool textTest; extern bool useMasterTrans; extern bool dumpText; + extern bool dumpRuntimeTexture; extern bool enableFreeCamera; extern int targetFrameRate; extern bool unlockAllLive; diff --git a/src/gkmsGUI/GUII18n.cpp b/src/gkmsGUI/GUII18n.cpp index 4a3db43..2e9c16f 100644 --- a/src/gkmsGUI/GUII18n.cpp +++ b/src/gkmsGUI/GUII18n.cpp @@ -2,19 +2,23 @@ #include "i18nData/strings_en.hpp" #include "i18nData/strings_ja.hpp" #include "i18nData/strings_zh-rCN.hpp" +#include "i18nData/strings_zh-rTW.hpp" namespace GkmsGUII18n { const LANGID localLanguage = GetUserDefaultUILanguage(); std::unordered_set sChineseLangIds{ { 0x0004, 0x0804, 0x1004 } }; // zh-Hans, zh-CN, zh-SG - // std::unordered_set tChineseLangIds{ { 0x0404, 0x0c04, 0x1404, 0x048E } }; // zh-TW, zh-HK, zh-MO, zh-yue-HK + std::unordered_set tChineseLangIds{ { 0x0404, 0x0c04, 0x1404, 0x048E } }; // zh-TW, zh-HK, zh-MO, zh-yue-HK std::unordered_set jpnLangIds{ { 0x0011, 0x0411 } }; // ja, ja-JP std::unordered_map GetI18nData() { if (sChineseLangIds.contains(localLanguage)) { return I18nData::i18nData_zh_rCN; } + else if (tChineseLangIds.contains(localLanguage)) { + return I18nData::i18nData_zh_rTW; + } else if (jpnLangIds.contains(localLanguage)) { return I18nData::i18nData_ja; } diff --git a/src/gkmsGUI/gkmsGUILoop.cpp b/src/gkmsGUI/gkmsGUILoop.cpp index a3b8d11..624a0b0 100644 --- a/src/gkmsGUI/gkmsGUILoop.cpp +++ b/src/gkmsGUI/gkmsGUILoop.cpp @@ -153,6 +153,7 @@ namespace GkmsGUILoop { if (ImGui::Begin("Gakumas Plugin Config")) { ImGui::Text("Plugin Version: %s", PLUGIN_VERSION); ImGui::Text("Resource Version: %s", GkmsResourceUpdate::GetCurrentResourceVersion(true).c_str()); + ImGui::Text("%s: %s", ts("texture_version"), GkmsResourceUpdate::GetCurrentTextureVersion(true).c_str()); if (ImGui::Button("Reload Config And Translation Data")) { g_reload_all_data(); @@ -169,6 +170,7 @@ namespace GkmsGUILoop { ImGui::Checkbox(ts("lazy_init"), &Config::lazyInit); ImGui::Checkbox(ts("replace_font"), &Config::replaceFont); + ImGui::Checkbox(ts("replace_texture"), &Config::replaceTexture); ImGui::Unindent(indentWidth); } @@ -207,6 +209,24 @@ namespace GkmsGUILoop { } } + if (Config::replaceTexture) { + ImGui::Separator(); + ImGui::Text("%s: %s", ts("downloaded_texture_version"), GkmsResourceUpdate::GetCurrentTextureVersion(true).c_str()); + ImGui::Checkbox(ts("check_texture_from_api"), &g_useAPITextureAssets); + if (g_useAPITextureAssets) { + ImGui::Checkbox(ts("del_texture_remote_after_update"), &g_delTextureRemoteAfterUpdate); + InputTextString(ts("texture_api_addr"), &g_useAPITextureAssetsURL); + if (!downloading && ImGui::Button((std::string(ts("check_update")) + "##APITexture").c_str())) { + GkmsResourceUpdate::CheckTextureUpdateFromAPI(true); + } + if (downloading) { + ImGui::ProgressBar(downloadProgress); + ImGui::SameLine(); + ImGui::Text("Downloading"); + } + } + } + ImGui::Unindent(indentWidth); } @@ -308,6 +328,7 @@ namespace GkmsGUILoop { ImGui::Checkbox(ts("useMasterDBTrans"), &Config::useMasterTrans); ImGui::Checkbox(ts("text_hook_test_mode"), &Config::textTest); ImGui::Checkbox(ts("export_text"), &Config::dumpText); + ImGui::Checkbox(ts("dump_runtime_texture"), &Config::dumpRuntimeTexture); ImGui::Checkbox(ts("login_as_ios"), &Config::loginAsIOS); ImGui::Unindent(indentWidth); diff --git a/src/gkmsGUI/i18nData/strings_en.hpp b/src/gkmsGUI/i18nData/strings_en.hpp index 08153dc..94fa3a1 100644 --- a/src/gkmsGUI/i18nData/strings_en.hpp +++ b/src/gkmsGUI/i18nData/strings_en.hpp @@ -13,6 +13,7 @@ namespace I18nData { { "gakumas_localify", "Gakumas Localify" }, { "enable_plugin", "Enable Plugin (Not Hot Reloadable)" }, { "replace_font", "Replace Font" }, + { "replace_texture", "Replace Texture" }, { "lazy_init", "Fast Initialization (Lazy loading)" }, { "enable_free_camera", "Enable Free Camera" }, { "start_game", "Start Game / Hot Reload Config" }, @@ -27,6 +28,7 @@ namespace I18nData { { "text_hook_test_mode", "Text Hook Test Mode" }, { "useMasterDBTrans", "Enable MasterDB Localization" }, { "export_text", "Export Text" }, + { "dump_runtime_texture", "Dump Runtime Texture" }, { "force_export_resource", "Force Update Resource" }, { "login_as_ios", "Login as iOS" }, { "max_high", "Ultra" }, @@ -88,13 +90,18 @@ namespace I18nData { { "cancel", "Cancel" }, { "ok", "OK" }, { "downloaded_resource_version", "Downloaded Version" }, + { "downloaded_texture_version", "Downloaded Texture Version" }, { "del_remote_after_update", "Delete Cache File After Update" }, + { "del_texture_remote_after_update", "Delete Cache File After Update" }, { "warning", "Warning" }, { "install", "Install" }, { "installing", "Installing" }, { "check_resource_from_api", "Check Resource Update From API" }, + { "check_texture_from_api", "Check Texture Update From API" }, { "api_addr", "API Address(Github Latest Release API)" }, + { "texture_api_addr", "Texture API Address(Github Latest Release API)" }, { "check_update", "Check" }, + { "texture_version", "Texture Version" }, { "translation_resource_update", "Translation Resource Update" }, { "game_patch", "Game Patch" }, { "patch_mode", "Patch Mode" }, @@ -114,4 +121,4 @@ namespace I18nData { { "about_contributors_asset_file", "about_contributors_en.json" }, { "default_assets_check_api", "https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest" }, }; -} \ No newline at end of file +} diff --git a/src/gkmsGUI/i18nData/strings_ja.hpp b/src/gkmsGUI/i18nData/strings_ja.hpp index 779aedd..48cd54d 100644 --- a/src/gkmsGUI/i18nData/strings_ja.hpp +++ b/src/gkmsGUI/i18nData/strings_ja.hpp @@ -37,6 +37,7 @@ namespace I18nData { { "character_counter_pattern", "%1$d/%2$d" }, { "check_built_in_resource", "内蔵アセットのアップデートを確認" }, { "check_resource_from_api", "リソースアップデートを API から確認" }, + { "check_texture_from_api", "テクスチャアップデートを API から確認" }, { "check_update", "確認" }, { "clear_text_end_icon_content_description", "テキストを消去" }, { "close_drawer", "ナビゲーションメニューを閉じる" }, @@ -48,9 +49,11 @@ namespace I18nData { { "default_error_message", "入力が無効です" }, { "default_popup_window_title", "ポップアップウィンドウ" }, { "del_remote_after_update", "キャッシュファイルをアップデート後に削除" }, + { "del_texture_remote_after_update", "キャッシュファイルをアップデート後に削除" }, { "delete_plugin_resource", "プラグインリソースを削除" }, { "download", "ダウンロード" }, { "downloaded_resource_version", "ダウンロードされたバージョン" }, + { "downloaded_texture_version", "ダウンロードされたテクスチャバージョン" }, { "dropdown_menu", "ドロップダウンメニュー" }, { "enable_breast_param", "胸のパラメーターを有効化" }, { "enable_free_camera", "フリーカメラを有効化" }, @@ -58,6 +61,7 @@ namespace I18nData { { "error_a11y_label", "エラー: 無効" }, { "error_icon_content_description", "エラー" }, { "export_text", "テキストをエクスポート" }, + { "dump_runtime_texture", "ランタイムテクスチャをダンプ" }, { "exposed_dropdown_menu_content_description", "ドロップダウンメニューを表示" }, { "force_export_resource", "リソースのアップデートを強制する" }, { "login_as_ios", "iOSとしてログイン" }, @@ -108,6 +112,7 @@ namespace I18nData { { "range_start", "範囲の開始" }, { "renderscale", "RenderScale (0.5/0.59/0.67/0.77/1.0)" }, { "replace_font", "フォントを置換する" }, + { "replace_texture", "テクスチャを置換する" }, { "reserve_patched", "パッチ済みの APK を予約する" }, { "resource_settings", "リソース設定" }, { "resource_url", "リソース URL" }, @@ -128,6 +133,8 @@ namespace I18nData { { "useMasterDBTrans", "MasterDB をローカライズする" }, { "translation_repository", "翻訳のリポジトリ" }, { "translation_resource_update", "翻訳リソースをアップデート" }, + { "texture_api_addr", "テクスチャ API アドレス (GitHub の最新リリース API)" }, + { "texture_version", "Texture version" }, { "unlockAllLive", "すべてのライブを開放" }, { "unlockAllLiveCostume", "すべてのライブ衣装を開放" }, { "useCustomeGraphicSettings", "カスタムグラフィック設定を使用する" }, @@ -139,4 +146,4 @@ namespace I18nData { { "very_high", "最高" }, { "warning", "警告" }, }; -} \ No newline at end of file +} diff --git a/src/gkmsGUI/i18nData/strings_zh-rCN.hpp b/src/gkmsGUI/i18nData/strings_zh-rCN.hpp index 4601e9b..f9c890c 100644 --- a/src/gkmsGUI/i18nData/strings_zh-rCN.hpp +++ b/src/gkmsGUI/i18nData/strings_zh-rCN.hpp @@ -13,6 +13,7 @@ namespace I18nData { { "gakumas_localify", "Gakumas Localify" }, { "enable_plugin", "启用插件 (不可热重载)" }, { "replace_font", "替换字体" }, + { "replace_texture", "替换贴图" }, { "lazy_init", "快速初始化(懒加载配置)" }, { "enable_free_camera", "启用自由视角(可热重载; 需使用实体键盘)" }, { "start_game", "以上述配置启动游戏/重载配置" }, @@ -27,6 +28,7 @@ namespace I18nData { { "text_hook_test_mode", "文本 hook 测试模式" }, { "useMasterDBTrans", "使用 MasterDB 本地化" }, { "export_text", "导出文本" }, + { "dump_runtime_texture", "导出运行时贴图" }, { "force_export_resource", "启动后强制导出资源" }, { "login_as_ios", "以 iOS 登陆" }, { "max_high", "极高" }, @@ -88,13 +90,18 @@ namespace I18nData { { "cancel", "取消" }, { "ok", "确定" }, { "downloaded_resource_version", "已下载资源版本" }, + { "downloaded_texture_version", "已下载贴图资源版本" }, { "del_remote_after_update", "替换文件后删除下载缓存" }, + { "del_texture_remote_after_update", "替换文件后删除下载缓存" }, { "warning", "注意" }, { "install", "安装" }, { "installing", "安装中" }, { "check_resource_from_api", "从服务器检查热更新资源" }, + { "check_texture_from_api", "从服务器检查贴图资源更新" }, { "api_addr", "API 地址(Github Latest Release API)" }, + { "texture_api_addr", "贴图 API 地址(Github Latest Release API)" }, { "check_update", "检查更新" }, + { "texture_version", "Texture version" }, { "translation_resource_update", "翻译资源更新" }, { "game_patch", "游戏修补" }, { "patch_mode", "修补模式" }, @@ -114,4 +121,4 @@ namespace I18nData { { "about_contributors_asset_file", "about_contributors_zh_cn.json" }, { "default_assets_check_api", "https://uma.chinosk6.cn/api/gkms_trans_data" }, }; -} \ No newline at end of file +} diff --git a/src/gkmsGUI/i18nData/strings_zh-rTW.hpp b/src/gkmsGUI/i18nData/strings_zh-rTW.hpp new file mode 100644 index 0000000..13fe5cf --- /dev/null +++ b/src/gkmsGUI/i18nData/strings_zh-rTW.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include + +namespace I18nData { + static const std::unordered_map i18nData_zh_rTW = { + { "local_file_already_latest", "本機檔案已經是最新版本,是否繼續更新?" }, + { "dmmUnlockSize", "解鎖視窗大小" }, + { "dmmUnlockSizeHelp", "可隨意拖動視窗大小。使用 F11 切換全螢幕。" }, + + { "app_name", "Gakumas Localify" }, + { "gakumas_localify", "Gakumas Localify" }, + { "enable_plugin", "啟用插件 (不可熱重載)" }, + { "replace_font", "替換字體" }, + { "replace_texture", "替換貼圖" }, + { "lazy_init", "快速初始化(懶人設定)" }, + { "enable_free_camera", "啟用自由視角(可熱重載; 需使用實體鍵盤)" }, + { "start_game", "以上述設定啟動遊戲/重載設定" }, + { "setFpsTitle", "最大 FPS (0 為保持遊戲原設定)" }, + { "unlockAllLive", "解鎖所有 Live" }, + { "unlockAllLiveCostume", "解鎖所有 Live 服裝" }, + { "liveUseCustomeDress", "Live 使用自定義角色" }, + { "live_costume_head_id", "Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)" }, + { "live_custome_dress_id", "Live 自定義服裝 ID (例: hski-cstm-0002)" }, + { "useCustomeGraphicSettings", "使用自定義畫質設定" }, + { "renderscale", "RenderScale (0.5/0.59/0.67/0.77/1.0)" }, + { "text_hook_test_mode", "文本 hook 測試模式" }, + { "useMasterDBTrans", "使用 MasterDB 翻譯" }, + { "export_text", "導出文本" }, + { "dump_runtime_texture", "導出運行時貼圖" }, + { "force_export_resource", "啟動後強制導出資源" }, + { "login_as_ios", "模擬以 iOS 登入" }, + { "max_high", "極高" }, + { "very_high", "超高" }, + { "hign", "高" }, + { "middle", "中" }, + { "low", "低" }, + { "orientation_orig", "原版" }, + { "orientation_portrait", "豎屏" }, + { "orientation_landscape", "橫屏" }, + { "orientation_lock", "方向鎖定" }, + { "enable_breast_param", "啟用胸部參數" }, + { "damping", "阻尼 (Damping)" }, + { "stiffness", "剛度 (Stiffness)" }, + { "spring", "彈簧係數 (Spring)" }, + { "pendulum", "鐘擺係數 (Pendulum)" }, + { "pendulumrange", "鐘擺範圍 (PendulumRange)" }, + { "average", "Average" }, + { "rootweight", "RootWeight" }, + { "uselimit_0_1", "範圍限制倍率 (0 為不限制, 1 為原版)" }, + { "usearmcorrection", "使用手臂矯正" }, + { "isdirty", "IsDirty" }, + { "usescale", "應用縮放" }, + { "breast_scale", "胸部縮放倍率" }, + { "uselimitmultiplier", "啟用範圍限制倍率" }, + { "axisx_x", "axisX.x" }, + { "axisy_x", "axisY.x" }, + { "axisz_x", "axisZ.x" }, + { "axisx_y", "axisX.y" }, + { "axisy_y", "axisY.y" }, + { "axisz_y", "axisZ.y" }, + { "basic_settings", "基本設定" }, + { "graphic_settings", "畫面設定" }, + { "camera_settings", "攝影機設定" }, + { "test_mode_live", "測試模式 - LIVE" }, + { "debug_settings", "調試設定" }, + { "breast_param", "胸部參數" }, + { "about", "關於" }, + { "home", "主頁" }, + { "advanced_settings", "進階設定" }, + { "about_warn_title", "使用前警告" }, + { "about_warn_p1", "本插件僅供學習和交流使用。" }, + { "about_warn_p2", "使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。" }, + { "about_about_title", "關於本插件" }, + { "about_about_p1", "本插件完全免費。若您付費購買了本插件,請檢舉店家。" }, + { "about_about_p2", "插件交流QQ群: 991990192" }, + { "project_contribution", "項目貢獻" }, + { "plugin_code", "插件本體" }, + { "contributors", "貢獻者列表" }, + { "translation_repository", "譯文倉庫" }, + { "resource_settings", "資源設定" }, + { "check_built_in_resource", "檢查內置翻譯資源更新" }, + { "delete_plugin_resource", "清除遊戲目錄內的插件翻譯資源" }, + { "use_remote_zip_resource", "使用雲端 ZIP 翻譯資源" }, + { "resource_url", "資源地址" }, + { "download", "下載" }, + { "invalid_zip_file", "文件解析失敗" }, + { "invalid_zip_file_warn", "此 ZIP 文件不是一個有效的翻譯資源包" }, + { "cancel", "取消" }, + { "ok", "確定" }, + { "downloaded_resource_version", "已下載資源版本" }, + { "downloaded_texture_version", "已下載貼圖資源版本" }, + { "del_remote_after_update", "替換文件後刪除下載緩存" }, + { "del_texture_remote_after_update", "替換文件後刪除下載緩存" }, + { "warning", "注意" }, + { "install", "安裝" }, + { "installing", "安裝中" }, + { "check_resource_from_api", "从伺服器檢查更新資源" }, + { "check_texture_from_api", "从伺服器檢查貼圖資源更新" }, + { "api_addr", "API 地址(Github Latest Release API)" }, + { "texture_api_addr", "貼圖 API 地址(Github Latest Release API)" }, + { "check_update", "檢查更新" }, + { "texture_version", "Texture version" }, + { "translation_resource_update", "翻譯資源更新" }, + { "game_patch", "遊戲修補" }, + { "patch_mode", "修補模式" }, + { "patch_local", "本地模式" }, + { "patch_local_desc", "為未嵌入模塊的遊戲程式打補丁。\\nXposed 範圍可動態更改,無需重新打補丁。\\n以本地模式修補的遊戲程式只能在本地設備上執行。" }, + { "patch_integrated", "集成模式" }, + { "patch_integrated_desc", "修補遊戲程式並內置模塊。\\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。" }, + { "shizuku_available", "Shizuku 服務可用" }, + { "shizuku_unavailable", "Shizuku 服務未連接" }, + { "home_shizuku_warning", "部分功能不可用" }, + { "patch_debuggable", "可調試" }, + { "reserve_patched", "安裝時保留修補包" }, + { "support_file_types", "支援文件類型:\\n單/多選 apk\\n單選 apks, xapk, zip" }, + { "patch_uninstall_text", "由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\\n請確保您已備份好個人資料。" }, + { "patch_uninstall_confirm", "您確定要刪除吗" }, + { "patch_finished", "修補完成,是否開始安裝?" }, + { "about_contributors_asset_file", "about_contributors_zh_cn.json" }, + { "default_assets_check_api", "https://uma.chinosk6.cn/api/gkms_trans_data" }, + }; +} diff --git a/src/main.cpp b/src/main.cpp index ba192e8..1bd6769 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,8 +25,11 @@ bool g_has_config_file = false; bool g_enable_console = true; bool g_useRemoteAssets = false; bool g_useAPIAssets = false; +bool g_useAPITextureAssets = false; +bool g_delTextureRemoteAfterUpdate = true; std::string g_remoteResourceUrl = ""; std::string g_useAPIAssetsURL = ""; +std::string g_useAPITextureAssetsURL = "https://texture.gakumas.cn/api/gkms_texture_data"; namespace { @@ -84,6 +87,18 @@ void readProgramConfig() { g_useAPIAssetsURL = document["useAPIAssetsURL"].GetString(); } + if (document.HasMember("useAPITextureAssets")) { + g_useAPITextureAssets = document["useAPITextureAssets"].GetBool(); + } + + if (document.HasMember("useAPITextureAssetsURL")) { + g_useAPITextureAssetsURL = document["useAPITextureAssetsURL"].GetString(); + } + + if (document.HasMember("delTextureRemoteAfterUpdate")) { + g_delTextureRemoteAfterUpdate = document["delTextureRemoteAfterUpdate"].GetBool(); + } + } config_stream.close(); } @@ -142,7 +157,7 @@ int __stdcall DllMain(HINSTANCE dllModule, DWORD reason, LPVOID) std::condition_variable cond; std::atomic hookIsReady(false); - // 依赖检查游戏版本的指针加载,因此在 hook 完成后再加载翻译数据 + // 渚濊禆妫€鏌ユ父鎴忕増鏈殑鎸囬拡鍔犺浇锛屽洜姝ゅ湪 hook 瀹屾垚鍚庡啀鍔犺浇缈昏瘧鏁版嵁 std::unique_lock lock(mutex); cond.wait(lock, [&] { return hookIsReady.load(std::memory_order_acquire); diff --git a/src/platformDefine.hpp b/src/platformDefine.hpp index 4f87c06..29bd682 100644 --- a/src/platformDefine.hpp +++ b/src/platformDefine.hpp @@ -11,7 +11,7 @@ #define LogMinVersion ANDROID_LOG_DEBUG -#define PLUGIN_VERSION "3.2.0" +#define PLUGIN_VERSION "v3.3.1" #define ADD_HOOK(name, addr) \ name##_Addr = reinterpret_cast(addr); \ diff --git a/src/resourceUpdate/resourceUpdate.cpp b/src/resourceUpdate/resourceUpdate.cpp index c3a89c2..40da274 100644 --- a/src/resourceUpdate/resourceUpdate.cpp +++ b/src/resourceUpdate/resourceUpdate.cpp @@ -1,10 +1,14 @@ -#include "stdinclude.hpp" +#include "stdinclude.hpp" #include "cpprest/http_client.h" #include "cpprest/filestream.h" #include "nlohmann/json.hpp" #include "GakumasLocalify/Log.h" #include "gkmsGUI/GUII18n.hpp" +#include +#include +#include #include +#include #include "unzip.hpp" extern std::filesystem::path gakumasLocalPath; @@ -13,8 +17,82 @@ extern bool downloading; extern float downloadProgress; extern std::function g_reload_all_data; std::string resourceVersionCache = ""; +std::string textureVersionCache = ""; namespace GkmsResourceUpdate { + std::atomic_bool updateJobRunning = false; + + class UpdateJobGuard { + public: + explicit UpdateJobGuard(const char* jobName) : active(false) { + bool expected = false; + if (!updateJobRunning.compare_exchange_strong(expected, true)) { + GakumasLocal::Log::InfoFmt("Skip %s: another resource update job is running.", jobName); + return; + } + active = true; + downloading = true; + downloadProgress = 0.0f; + } + + ~UpdateJobGuard() { + if (!active) return; + downloading = false; + updateJobRunning = false; + } + + explicit operator bool() const { + return active; + } + + private: + bool active; + }; + + std::string trimString(std::string content) { + auto is_not_space = [](unsigned char ch) { + return !std::isspace(ch); + }; + content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space)); + content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end()); + return content; + } + + std::string readTrimmedFile(const std::filesystem::path& filePath) { + std::ifstream file(filePath); + if (!file) { + return "Unknown"; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + return trimString(buffer.str()); + } + + std::filesystem::path getTextureResourceRoot() { + return gakumasLocalPath / "texture2d"; + } + + std::filesystem::path findTextureZipSourceDir(const std::filesystem::path& extractDir) { + std::error_code ec; + std::filesystem::path fallback; + for (const auto& entry : std::filesystem::recursive_directory_iterator( + extractDir, std::filesystem::directory_options::skip_permission_denied, ec)) { + if (ec) break; + if (!entry.is_regular_file(ec)) continue; + if (entry.path().filename() != "texture_version.txt") continue; + + const auto parent = entry.path().parent_path(); + if (parent.filename() == "texture2d") { + return parent; + } + if (fallback.empty()) { + fallback = parent; + } + } + return fallback; + } + void saveProgramConfig() { nlohmann::json config; config["enableConsole"] = g_enable_console; @@ -22,6 +100,9 @@ namespace GkmsResourceUpdate { config["transRemoteZipUrl"] = g_remoteResourceUrl; config["useAPIAssets"] = g_useAPIAssets; config["useAPIAssetsURL"] = g_useAPIAssetsURL; + config["useAPITextureAssets"] = g_useAPITextureAssets; + config["useAPITextureAssetsURL"] = g_useAPITextureAssetsURL; + config["delTextureRemoteAfterUpdate"] = g_delTextureRemoteAfterUpdate; std::ofstream out(ProgramConfigJson); if (!out) { @@ -47,18 +128,18 @@ namespace GkmsResourceUpdate { using namespace concurrency::streams; try { - // ļͬʽ + // 打开输出文件流(同步方式) auto outTask = fstream::open_ostream(conversions::to_string_t(outputPath)); outTask.wait(); auto fileStream = outTask.get(); - // HTTP ͻˣע⣺ url ·cpprestsdk Զ + // 创建 HTTP 客户端,注意:如果 url 包含完整路径,cpprestsdk 会自动解析 http_client client(conversions::to_string_t(url)); downloading = true; downloadProgress = 0.0f; - // GET + // 发起 GET 请求 auto responseTask = client.request(methods::GET); responseTask.wait(); http_response response = responseTask.get(); @@ -68,12 +149,12 @@ namespace GkmsResourceUpdate { return false; } - // ȡӦͷеļСڣ + // 获取响应头中的文件大小(如果存在) uint64_t contentLength = 0; if (response.headers().has(L"Content-Length")) contentLength = std::stoull(conversions::to_utf8string(response.headers().find(L"Content-Length")->second)); - // ȡӦ壬дļͬʱ½ + // 读取响应体,逐块写入文件,同时更新进度 auto inStream = response.body(); const size_t bufferSize = 8192; // std::vector buffer(bufferSize); @@ -108,26 +189,20 @@ namespace GkmsResourceUpdate { } auto resourceVersionFile = gakumasLocalPath / "version.txt"; - std::ifstream file(resourceVersionFile); - if (!file) { - // GakumasLocal::Log::ErrorFmt("Can't open file: %s", resourceVersionFile.string().c_str()); - return "Unknown"; + resourceVersionCache = readTrimmedFile(resourceVersionFile); + return resourceVersionCache; + } + + std::string GetCurrentTextureVersion(bool useCache) { + if (useCache) { + if (!textureVersionCache.empty()) { + return textureVersionCache; + } } - std::stringstream buffer; - buffer << file.rdbuf(); - std::string content = buffer.str(); - - // ȥβոͻз - auto is_not_space = [](unsigned char ch) { - return !std::isspace(ch); - }; - // ȥǰհ - content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space)); - // ȥβհ - content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end()); - resourceVersionCache = content; - return content; + auto textureVersionFile = getTextureResourceRoot() / "texture_version.txt"; + textureVersionCache = readTrimmedFile(textureVersionFile); + return textureVersionCache; } bool unzipFileFromURL(std::string downloadUrl, const std::string& unzipPath, const std::string& targetDir = "") { @@ -146,12 +221,106 @@ namespace GkmsResourceUpdate { return true; } + bool installTextureZipFromURL(const std::string& downloadUrl, const std::string& expectedVersion) { + auto textureRoot = getTextureResourceRoot(); + auto tempZipFile = gakumasLocalPath / "temp_texture_download.zip"; + auto extractDir = gakumasLocalPath / "texture2d.extract"; + auto installDir = gakumasLocalPath / "texture2d.tmp"; + + std::error_code ec; + std::filesystem::remove(tempZipFile, ec); + std::filesystem::remove_all(extractDir, ec); + std::filesystem::remove_all(installDir, ec); + std::filesystem::create_directories(gakumasLocalPath, ec); + + if (!DownloadFile(downloadUrl, tempZipFile.string())) { + GakumasLocal::Log::Error("Download texture zip file failed."); + return false; + } + + if (!UnzipFile(tempZipFile.string(), extractDir.string())) { + GakumasLocal::Log::Error("Unzip texture file failed."); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + + auto sourceDir = findTextureZipSourceDir(extractDir); + if (sourceDir.empty()) { + GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt not found."); + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + + auto versionFile = sourceDir / "texture_version.txt"; + const auto packageVersion = readTrimmedFile(versionFile); + if (packageVersion.empty() || packageVersion == "Unknown") { + GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt is empty or not found."); + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + if (!expectedVersion.empty() && packageVersion != expectedVersion) { + GakumasLocal::Log::ErrorFmt( + "Texture zip validation failed: texture_version.txt (%s) differs from release tag (%s).", + packageVersion.c_str(), expectedVersion.c_str()); + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + + std::filesystem::copy(sourceDir, installDir, + std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, ec); + if (ec) { + GakumasLocal::Log::ErrorFmt("Copy texture resource failed: %s", ec.message().c_str()); + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + + std::filesystem::remove_all(textureRoot, ec); + ec.clear(); + std::filesystem::rename(installDir, textureRoot, ec); + if (ec) { + GakumasLocal::Log::ErrorFmt("Install texture resource failed: %s", ec.message().c_str()); + std::filesystem::remove_all(installDir, ec); + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + return false; + } + + std::filesystem::remove_all(extractDir, ec); + if (g_delTextureRemoteAfterUpdate) { + std::filesystem::remove(tempZipFile, ec); + } + + textureVersionCache.clear(); + const auto installedVersion = GetCurrentTextureVersion(false); + GakumasLocal::Log::InfoFmt("Texture zip installed into %s, version=%s", + textureRoot.string().c_str(), installedVersion.c_str()); + return true; + } + void CheckUpdateFromAPI(bool isManual) { std::thread([isManual]() { try { if (!g_useAPIAssets) { return; } + UpdateJobGuard job("resource API update"); + if (!job) return; GakumasLocal::Log::Info("Checking update from API..."); @@ -204,7 +373,7 @@ namespace GkmsResourceUpdate { g_reload_all_data(); GakumasLocal::Log::Info("Update completed."); } - // ѹһļ + // 仅解压一个文件 return; } } @@ -217,8 +386,100 @@ namespace GkmsResourceUpdate { }).detach(); } + void CheckTextureUpdateFromAPI(bool isManual) { + std::thread([isManual]() { + try { + if (!g_useAPITextureAssets) { + return; + } + UpdateJobGuard job("texture API update"); + if (!job) return; + + GakumasLocal::Log::Info("Checking texture update from API..."); + + auto response = send_get(g_useAPITextureAssetsURL, 30); + if (response.status_code() != 200) { + GakumasLocal::Log::ErrorFmt("Failed to check texture update from API: %d\n", response.status_code()); + return; + } + + auto data = nlohmann::json::parse(response.extract_utf8string().get()); + if (!data.contains("tag_name") || !data["tag_name"].is_string()) { + GakumasLocal::Log::Error("Texture API response doesn't contain tag_name."); + return; + } + + std::string remoteVersion = data["tag_name"]; + const auto localVersion = GetCurrentTextureVersion(false); + + if (localVersion == remoteVersion) { + if (isManual) { + auto check = MessageBoxW(NULL, utility::conversions::to_string_t(GkmsGUII18n::ts("local_file_already_latest")).c_str(), + L"Texture Resource Update", MB_OKCANCEL); + if (check != IDOK) { + return; + } + } + else { + return; + } + } + + std::string description = ""; + if (data.contains("body") && data["body"].is_string()) { + description = data["body"]; + } + + auto check = MessageBoxW(NULL, std::format(L"{} -> {}\n\n{}", utility::conversions::to_string_t(localVersion), + utility::conversions::to_string_t(remoteVersion), utility::conversions::to_string_t(description)).c_str(), + L"Texture Resource Update", MB_OKCANCEL); + if (check != IDOK) { + return; + } + + if (!data.contains("assets") || !data["assets"].is_array()) { + GakumasLocal::Log::Error("Texture API response doesn't contain assets array."); + return; + } + + std::string downloadUrl = ""; + for (const auto& asset : data["assets"]) { + if (!asset.contains("name") || !asset.contains("browser_download_url") || !asset["name"].is_string() || !asset["browser_download_url"].is_string()) { + continue; + } + std::string name = asset["name"]; + std::string lowerName = name; + std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + if (lowerName.ends_with(".zip")) { + downloadUrl = asset["browser_download_url"]; + if (lowerName == "texture2d.zip") { + break; + } + } + } + + if (downloadUrl.empty()) { + GakumasLocal::Log::Error("No texture .zip file found."); + return; + } + + if (installTextureZipFromURL(downloadUrl, remoteVersion)) { + g_reload_all_data(); + GakumasLocal::Log::Info("Texture update completed."); + } + } + catch (std::exception& e) { + GakumasLocal::Log::ErrorFmt("Exception occurred in CheckTextureUpdateFromAPI: %s\n", e.what()); + } + }).detach(); + } + void checkUpdateFromURL(const std::string& downloadUrl) { std::thread([downloadUrl]() { + UpdateJobGuard job("remote zip update"); + if (!job) return; if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) { g_reload_all_data(); GakumasLocal::Log::Info("Update completed."); diff --git a/src/resourceUpdate/resourceUpdate.hpp b/src/resourceUpdate/resourceUpdate.hpp index d9c81b4..6681619 100644 --- a/src/resourceUpdate/resourceUpdate.hpp +++ b/src/resourceUpdate/resourceUpdate.hpp @@ -1,10 +1,12 @@ -#pragma once +#pragma once #include namespace GkmsResourceUpdate { void saveProgramConfig(); std::string GetCurrentResourceVersion(bool useCache); + std::string GetCurrentTextureVersion(bool useCache); void CheckUpdateFromAPI(bool isManual); + void CheckTextureUpdateFromAPI(bool isManual); void checkUpdateFromURL(const std::string& downloadUrl); -} \ No newline at end of file +} diff --git a/src/stdinclude.hpp b/src/stdinclude.hpp index d9bbd23..a6a8445 100644 --- a/src/stdinclude.hpp +++ b/src/stdinclude.hpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once #define NOMINMAX @@ -38,10 +38,13 @@ extern bool g_has_config_file; -// config +// config 区 extern bool g_enable_console; extern bool g_useRemoteAssets; extern bool g_useAPIAssets; +extern bool g_useAPITextureAssets; +extern bool g_delTextureRemoteAfterUpdate; extern std::string g_remoteResourceUrl; extern std::string g_useAPIAssetsURL; -// config +extern std::string g_useAPITextureAssetsURL; +// config 区结束 diff --git a/src/windowsPlatform.cpp b/src/windowsPlatform.cpp index 7f765e8..75c3aed 100644 --- a/src/windowsPlatform.cpp +++ b/src/windowsPlatform.cpp @@ -1,4 +1,4 @@ -#include "windowsPlatform.hpp" +#include "windowsPlatform.hpp" #include "GakumasLocalify/Plugin.h" #include "GakumasLocalify/Log.h" #include "GakumasLocalify/Local.h" @@ -158,6 +158,7 @@ void reload_all_data() { readProgramConfig(); loadConfig(ConfigJson); GkmsResourceUpdate::GetCurrentResourceVersion(false); + GkmsResourceUpdate::GetCurrentTextureVersion(false); GakumasLocal::Local::LoadData(); GakumasLocal::MasterLocal::LoadData(); } @@ -234,7 +235,7 @@ namespace GakumasLocal::WinHooks { v8[0] = 0x100000000LL; if (currFullScreen) { - // ȡȫ + // 取消全屏 if (savedWidth == -1) { savedWidth = 542; savedHeight = 990; @@ -270,7 +271,7 @@ namespace GakumasLocal::WinHooks { UINT size = sizeof(RAWINPUT); if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawInput, &size, sizeof(RAWINPUTHEADER)) == size) { - /* ¼ + /* 鼠标事件,后面加上 if (rawInput.header.dwType == RIM_TYPEMOUSE) { switch (rawInput.data.mouse.ulButtons) { @@ -373,7 +374,7 @@ namespace GakumasLocal::WinHooks { ScreenToClient(hWnd, &pt); RECT rcClient; GetClientRect(hWnd, &rcClient); - const int borderWidth = 8; // ҪԵ + const int borderWidth = 8; // 根据需要调整边缘宽度 bool left = pt.x < borderWidth; bool right = pt.x >= rcClient.right - borderWidth; @@ -396,10 +397,10 @@ namespace GakumasLocal::WinHooks { { if (GakumasLocal::Config::dmmUnlockSize) { LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam; - // ߴΪĻֱʣͲƴڵߴ + // 设置最大尺寸为屏幕分辨率,这样就不限制窗口的最大尺寸 lpMMI->ptMaxTrackSize.x = GetSystemMetrics(SM_CXSCREEN) * 3; lpMMI->ptMaxTrackSize.y = GetSystemMetrics(SM_CYSCREEN) * 3; - // ѡôСߴ磨200x200 + // 可选:设置窗口最小尺寸(例如200x200) lpMMI->ptMinTrackSize.x = 200; lpMMI->ptMinTrackSize.y = 200; return 1; @@ -421,7 +422,7 @@ namespace GakumasLocal::WinHooks { // printf("WM_NCPAINT: 0x%x\n", style); if (!(style & WS_POPUP)) { - // ӿɵСı߿󻯰ť + // 添加可调整大小的边框和最大化按钮 style |= WS_THICKFRAME | WS_MAXIMIZEBOX; SetWindowLong(hWnd, GWL_STYLE, style); } @@ -439,7 +440,7 @@ namespace GakumasLocal::WinHooks { auto hWnd = FindWindowW(L"UnityWndClass", L"gakumas"); g_pfnOldWndProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC); SetWindowLongPtr(FindWindowW(L"UnityWndClass", L"gakumas"), GWLP_WNDPROC, (LONG_PTR)WndProcCallback); - // ӿɵСı߿󻯰ť + // 添加可调整大小的边框和最大化按钮 LONG style = GetWindowLong(hWnd, GWL_STYLE); style |= WS_THICKFRAME | WS_MAXIMIZEBOX; SetWindowLong(hWnd, GWL_STYLE, style); From 0073e27982d27af60d999679ce662fabc3d772da Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sun, 10 May 2026 16:26:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=88=86=E5=89=B2hook.cpp=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GakumasLocalify/Hook.cpp | 776 +-------------------------- src/GakumasLocalify/HookTexture.cpp | 797 ++++++++++++++++++++++++++++ src/GakumasLocalify/HookTexture.h | 23 + 3 files changed, 821 insertions(+), 775 deletions(-) create mode 100644 src/GakumasLocalify/HookTexture.cpp create mode 100644 src/GakumasLocalify/HookTexture.h diff --git a/src/GakumasLocalify/Hook.cpp b/src/GakumasLocalify/Hook.cpp index b197d67..a6b64d8 100644 --- a/src/GakumasLocalify/Hook.cpp +++ b/src/GakumasLocalify/Hook.cpp @@ -1,4 +1,5 @@ #include "Hook.h" +#include "HookTexture.h" #include "Plugin.h" #include "Log.h" #include "../deps/UnityResolve/UnityResolve.hpp" @@ -267,12 +268,6 @@ namespace GakumasLocal::HookMain { std::unordered_map loadHistory{}; - void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName); - void* ReplaceTextureOrSpriteByObjectName(void* result); - void ReplaceAllAssetTextures(void* allAssets); - void* ReplaceSpriteAssetByTextureName(void* sprite); - void* ReplaceSpriteTexture(void* texture2D); - void DumpTextureOrSpriteAsset(void* result); DEFINE_HOOK(void*, AssetBundle_LoadAsset, (void* self, Il2cppString* name, void* type)) { auto result = AssetBundle_LoadAsset_Orig(self, name, type); @@ -526,775 +521,6 @@ namespace GakumasLocal::HookMain { } #endif - Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr; - Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr; - std::unordered_map LoadedLocalTextureHandles{}; - std::unordered_set AppliedLocalTextureKeys{}; - - Il2cppUtils::Il2CppClassHead* GetTexture2DClass() { - if (!Texture2DClass) { - const auto textureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Texture2D"); - if (textureClass) { - Texture2DClass = static_cast(textureClass->address); - } - } - return Texture2DClass; - } - - Il2cppUtils::Il2CppClassHead* GetSpriteClass() { - if (!SpriteClass) { - const auto spriteClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite"); - if (spriteClass) { - SpriteClass = static_cast(spriteClass->address); - } - } - return SpriteClass; - } - - bool IsTexture2D(void* obj) { - const auto textureClass = GetTexture2DClass(); - if (!obj || !textureClass) return false; - - const auto objClass = Il2cppUtils::get_class_from_instance(obj); - if (objClass == textureClass) return true; - - return UnityResolve::Invoke("il2cpp_class_is_assignable_from", textureClass, objClass); - } - - bool IsSprite(void* obj) { - const auto spriteClass = GetSpriteClass(); - if (!obj || !spriteClass) return false; - - const auto objClass = Il2cppUtils::get_class_from_instance(obj); - if (objClass == spriteClass) return true; - - return UnityResolve::Invoke("il2cpp_class_is_assignable_from", spriteClass, objClass); - } - - Il2cppString* GetObjectName(void* obj) { - if (!obj) return nullptr; - - static auto Object_GetName = reinterpret_cast( - Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)")); - return Object_GetName ? Object_GetName(obj) : nullptr; - } - - void SetDontUnloadUnusedAsset(void* obj) { - if (!obj) return; - - static auto Object_set_hideFlags = reinterpret_cast( - Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Object", "set_hideFlags")); - if (Object_set_hideFlags) { - Object_set_hideFlags(obj, 32); - } - } - - void AddTexturePathCandidate(std::vector& candidates, const std::filesystem::path& path) { - if (path.empty()) return; - if (std::find(candidates.begin(), candidates.end(), path) == candidates.end()) { - candidates.emplace_back(path); - } - if (!path.has_extension()) { - auto pngPath = path; - pngPath += ".png"; - if (std::find(candidates.begin(), candidates.end(), pngPath) == candidates.end()) { - candidates.emplace_back(std::move(pngPath)); - } - } - } - - enum class TextureCategory { - Image, - Atlas, - Others, - }; - - std::string ToLowerAscii(std::string value) { - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { - return static_cast(std::tolower(ch)); - }); - return value; - } - - TextureCategory GetTextureCategory(const std::string& textureName) { - const auto lowerName = ToLowerAscii(std::filesystem::path(textureName).filename().generic_string()); - if (lowerName.rfind("img", 0) == 0) { - return TextureCategory::Image; - } - if (lowerName.rfind("sactx", 0) == 0) { - return TextureCategory::Atlas; - } - return TextureCategory::Others; - } - - std::filesystem::path GetTextureCategoryDirName(TextureCategory category) { - switch (category) { - case TextureCategory::Image: - return "image"; - case TextureCategory::Atlas: - return "atlas"; - default: - return "others"; - } - } - - std::filesystem::path GetTextureReplaceRoot() { - return Local::GetBasePath() / "texture2d"; - } - - std::filesystem::path GetTextureDumpRoot() { - return Local::GetBasePath() / "dump-files" / "texture2d"; - } - - std::filesystem::path GetTextureReplaceBase(const std::string& textureName) { - return GetTextureReplaceRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); - } - - std::filesystem::path GetTextureDumpBase(const std::string& textureName) { - return GetTextureDumpRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); - } - - std::vector SplitString(const std::string& value, char delimiter) { - std::vector parts; - size_t start = 0; - while (start <= value.size()) { - const auto end = value.find(delimiter, start); - parts.emplace_back(value.substr(start, end == std::string::npos ? std::string::npos : end - start)); - if (end == std::string::npos) break; - start = end + 1; - } - return parts; - } - - void AppendTextureCandidates(std::vector& target, std::vector&& source); - std::string NormalizeLocalAssetKey(const std::filesystem::path& path); - - bool IsHexHashPart(const std::string& value) { - return value.size() == 8 && std::all_of(value.begin(), value.end(), [](unsigned char ch) { - return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); - }); - } - - std::string GetPortableSactxTextureName(const std::string& objectName) { - auto fileName = std::filesystem::path(objectName).filename().generic_string(); - if (fileName.ends_with(".png")) { - fileName.resize(fileName.size() - 4); - } - - const auto parts = SplitString(fileName, '-'); - if (parts.size() < 5 || parts[0] != "sactx" || parts[2].find('x') == std::string::npos) { - return {}; - } - - const auto atlasEnd = IsHexHashPart(parts.back()) ? parts.size() - 1 : parts.size(); - if (atlasEnd <= 4) return {}; - - std::string portableName = parts[0] + "-" + parts[1] + "-" + parts[2]; - for (size_t i = 4; i < atlasEnd; ++i) { - portableName += "-" + parts[i]; - } - return portableName; - } - - std::unordered_map>> RecursiveTexturePathIndex{}; - - std::vector GetRecursiveTextureCandidates(const std::filesystem::path& basePath, - const std::string& lookupName) { - std::vector candidates; - if (lookupName.empty() || !std::filesystem::exists(basePath)) return candidates; - - const auto baseKey = NormalizeLocalAssetKey(basePath); - auto& index = RecursiveTexturePathIndex[baseKey]; - if (index.empty()) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(basePath)) { - if (!entry.is_regular_file()) continue; - - const auto& path = entry.path(); - if (ToLowerAscii(path.extension().generic_string()) != ".png") continue; - - const auto fileName = path.filename().generic_string(); - const auto stemName = path.stem().generic_string(); - index[fileName].emplace_back(path); - if (stemName != fileName) { - index[stemName].emplace_back(path); - } - } - } - - if (const auto iter = index.find(lookupName); iter != index.end()) { - candidates.insert(candidates.end(), iter->second.begin(), iter->second.end()); - } - return candidates; - } - - std::vector GetNamedTextureCandidates(const std::filesystem::path& assetName) { - std::vector candidates; - if (assetName.empty()) return candidates; - - const auto basePath = GetTextureReplaceBase(assetName.filename().generic_string()); - AddTexturePathCandidate(candidates, basePath / assetName); - if (assetName.has_parent_path()) { - AddTexturePathCandidate(candidates, basePath / assetName.filename()); - } - AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, assetName.filename().generic_string())); - - const auto portableAssetName = GetPortableSactxTextureName(assetName.filename().generic_string()); - if (!portableAssetName.empty()) { - AddTexturePathCandidate(candidates, basePath / portableAssetName); - AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableAssetName)); - } - return candidates; - } - - std::vector GetSpriteTextureCandidates(const std::string& objectName) { - std::vector candidates; - if (objectName.empty()) return candidates; - - auto safeObjectName = objectName; - std::replace(safeObjectName.begin(), safeObjectName.end(), '|', '_'); - - const auto basePath = GetTextureReplaceBase(safeObjectName); - AddTexturePathCandidate(candidates, basePath / objectName); - if (safeObjectName != objectName) { - AddTexturePathCandidate(candidates, basePath / safeObjectName); - } - AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, safeObjectName)); - const auto portableObjectName = GetPortableSactxTextureName(safeObjectName); - if (!portableObjectName.empty() && portableObjectName != objectName && portableObjectName != safeObjectName) { - AddTexturePathCandidate(candidates, basePath / portableObjectName); - AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableObjectName)); - } - return candidates; - } - - void AppendTextureCandidates(std::vector& target, std::vector&& source) { - target.insert(target.end(), - std::make_move_iterator(source.begin()), - std::make_move_iterator(source.end())); - } - - std::vector GetSpriteAssetTextureCandidates(void* sprite, const std::string& assetName) { - std::vector candidates; - - const auto assetPath = std::filesystem::path(assetName); - if (!assetName.empty()) { - AppendTextureCandidates(candidates, GetSpriteTextureCandidates(assetPath.filename().generic_string())); - } - - if (sprite && Sprite_get_texture_Orig) { - if (const auto texture = Sprite_get_texture_Orig(sprite)) { - if (const auto textureName = GetObjectName(texture)) { - AppendTextureCandidates(candidates, GetSpriteTextureCandidates(textureName->ToString())); - } - } - } - return candidates; - } - - std::string NormalizeLocalAssetKey(const std::filesystem::path& path) { - auto key = path.lexically_normal().generic_string(); - std::replace(key.begin(), key.end(), '\\', '/'); - return key; - } - - std::string SanitizeDumpPathPart(std::string part) { - constexpr std::string_view invalidChars = "<>:\"/\\|?*"; - if (part.empty() || part == "." || part == "..") return "_"; - - for (auto& ch : part) { - if (static_cast(ch) < 32 || invalidChars.find(ch) != std::string_view::npos) { - ch = '_'; - } - } - - while (!part.empty() && (part.back() == '.' || part.back() == ' ')) { - part.back() = '_'; - } - return part.empty() ? "_" : part; - } - - std::filesystem::path SanitizeDumpSubPath(const std::filesystem::path& dumpSubDir) { - std::filesystem::path safePath; - for (const auto& part : dumpSubDir) { - const auto partString = part.generic_string(); - if (partString.empty() || partString == "." || partString == ".." - || part == part.root_name() || part == part.root_directory()) { - continue; - } - safePath /= SanitizeDumpPathPart(partString); - } - return safePath; - } - - bool DumpTexture2D(void* texture2D) { - if (!IsTexture2D(texture2D)) return false; - - const auto objectName = GetObjectName(texture2D); - const auto textureName = objectName ? objectName->ToString() : std::string("texture"); - const auto dumpDir = GetTextureDumpBase(textureName); - const auto dumpPath = dumpDir / (SanitizeDumpPathPart(textureName) + ".png"); - - if (std::filesystem::exists(dumpPath)) return true; - - static auto Texture2D_get_width = [] { - const auto textureClass = GetTexture2DClass(); - const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_width", 0) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto Texture2D_get_height = [] { - const auto textureClass = GetTexture2DClass(); - const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_height", 0) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto Texture2D_ctor = [] { - const auto textureClass = GetTexture2DClass(); - const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; - return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; - }(); - static auto Texture2D_ReadPixels = [] { - const auto textureClass = GetTexture2DClass(); - const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "ReadPixels", 3) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto Texture2D_Apply = [] { - const auto textureClass = GetTexture2DClass(); - const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "Apply", 0) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto RenderTexture_GetTemporary = [] { - const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); - const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "GetTemporary", 3) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto RenderTexture_ReleaseTemporary = [] { - const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); - const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "ReleaseTemporary", 1) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto RenderTexture_get_active = [] { - const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); - const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "get_active", 0) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto RenderTexture_set_active = [] { - const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); - const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "set_active", 1) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto Graphics_Blit = [] { - const auto graphicsClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Graphics"); - const auto method = graphicsClass ? Il2cppUtils::il2cpp_class_get_method_from_name(graphicsClass->address, "Blit", 2) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - static auto ImageConversion_EncodeToPNG = [] { - using EncodeToPNGFn = void* (*)(void*); - if (const auto icall = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.ImageConversion::EncodeToPNG(UnityEngine.Texture2D)")) { - return reinterpret_cast(icall); - } - - for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { - const auto assembly = UnityResolve::Get(assemblyName); - const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; - const auto method = imageConversionClass - ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "EncodeToPNG", 1) - : nullptr; - if (method) { - return reinterpret_cast(method->methodPointer); - } - } - return static_cast(nullptr); - }(); - static auto File_WriteAllBytes = [] { - const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); - const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "WriteAllBytes", 2) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - - if (!Texture2D_get_width || !Texture2D_get_height || !Texture2D_ctor || !Texture2D_ReadPixels - || !Texture2D_Apply || !RenderTexture_GetTemporary || !RenderTexture_ReleaseTemporary - || !RenderTexture_get_active || !RenderTexture_set_active || !Graphics_Blit - || !ImageConversion_EncodeToPNG || !File_WriteAllBytes) { - Log::Error("DumpTexture2D failed: Unity texture dump API not found."); - return false; - } - - const auto width = Texture2D_get_width(texture2D); - const auto height = Texture2D_get_height(texture2D); - if (width <= 0 || height <= 0) return false; - - void* renderTexture = nullptr; - void* readableTexture = nullptr; - void* previousActive = nullptr; - const auto cleanup = [&] { - if (RenderTexture_get_active && RenderTexture_set_active - && (previousActive || RenderTexture_get_active() == renderTexture)) { - RenderTexture_set_active(previousActive); - } - if (renderTexture && RenderTexture_ReleaseTemporary) { - RenderTexture_ReleaseTemporary(renderTexture); - } - }; - - try { - std::filesystem::create_directories(dumpDir); - - renderTexture = RenderTexture_GetTemporary(width, height, 0); - if (!renderTexture) { - cleanup(); - return false; - } - - Graphics_Blit(texture2D, renderTexture); - previousActive = RenderTexture_get_active(); - RenderTexture_set_active(renderTexture); - - readableTexture = UnityResolve::Invoke("il2cpp_object_new", GetTexture2DClass()); - if (!readableTexture) { - cleanup(); - return false; - } - - Texture2D_ctor(readableTexture, width, height); - Texture2D_ReadPixels(readableTexture, UnityResolve::UnityType::Rect(0, 0, static_cast(width), static_cast(height)), 0, 0); - Texture2D_Apply(readableTexture); - - const auto pngBytes = ImageConversion_EncodeToPNG(readableTexture); - if (!pngBytes) { - cleanup(); - return false; - } - - File_WriteAllBytes(Il2cppString::New(dumpPath.string()), pngBytes); - Log::InfoFmt("Texture dumped: %s", dumpPath.string().c_str()); - cleanup(); - return true; - } - catch (const std::exception& ex) { - cleanup(); - Log::ErrorFmt("DumpTexture2D failed: %s", ex.what()); - return false; - } - catch (...) { - cleanup(); - Log::Error("DumpTexture2D failed: unknown error."); - return false; - } - } - - void DumpTextureOrSpriteAsset(void* result) { - if (!result) return; - - if (IsTexture2D(result)) { - DumpTexture2D(result); - return; - } - if (IsSprite(result) && Sprite_get_texture_Orig) { - if (const auto texture = Sprite_get_texture_Orig(result)) { - DumpTexture2D(texture); - } - } - } - - void* LoadLocalTexture2D(const std::filesystem::path& path) { - if (!std::filesystem::is_regular_file(path)) return nullptr; - - const auto cacheKey = NormalizeLocalAssetKey(path); - if (const auto iter = LoadedLocalTextureHandles.find(cacheKey); iter != LoadedLocalTextureHandles.end()) { - const auto cachedTexture = UnityResolve::Invoke("il2cpp_gchandle_get_target", iter->second); - if (cachedTexture && IsNativeObjectAlive(cachedTexture)) { - return cachedTexture; - } - - UnityResolve::Invoke("il2cpp_gchandle_free", iter->second); - LoadedLocalTextureHandles.erase(iter); - } - - const auto textureClass = GetTexture2DClass(); - if (!textureClass) return nullptr; - - static auto Texture2D_ctor = [] { - const auto textureClass = GetTexture2DClass(); - const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; - return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; - }(); - static auto ImageConversion_LoadImage = [] { - using LoadImageFn = bool (*)(void*, void*, bool); - if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { - return reinterpret_cast(icall); - } - - for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { - const auto assembly = UnityResolve::Get(assemblyName); - const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; - const auto method = imageConversionClass - ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) - : nullptr; - if (method) { - return reinterpret_cast(method->methodPointer); - } - } - return static_cast(nullptr); - }(); - static auto File_ReadAllBytes = [] { - const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); - const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - - if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) { - Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found."); - return nullptr; - } - - const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); - if (!fileBytes) return nullptr; - - const auto texture = UnityResolve::Invoke("il2cpp_object_new", textureClass); - Texture2D_ctor(texture, 2, 2); - if (!ImageConversion_LoadImage(texture, fileBytes, false)) { - Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str()); - return nullptr; - } - - SetDontUnloadUnusedAsset(texture); - LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke("il2cpp_gchandle_new", texture, false)); - Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str()); - return texture; - } - - void* LoadLocalTexture2DFromCandidates(const std::vector& candidates) { - for (const auto& candidate : candidates) { - if (auto texture = LoadLocalTexture2D(candidate)) { - return texture; - } - } - return nullptr; - } - - bool ApplyLocalImageToTexture2D(void* texture2D, const std::filesystem::path& path) { - if (!IsTexture2D(texture2D) || !std::filesystem::is_regular_file(path)) return false; - - auto cacheKey = NormalizeLocalAssetKey(path) - + "|" + std::to_string(reinterpret_cast(texture2D)); - if (AppliedLocalTextureKeys.contains(cacheKey)) return true; - - static auto ImageConversion_LoadImage = [] { - using LoadImageFn = bool (*)(void*, void*, bool); - if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { - return reinterpret_cast(icall); - } - - for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { - const auto assembly = UnityResolve::Get(assemblyName); - const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; - const auto method = imageConversionClass - ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) - : nullptr; - if (method) { - return reinterpret_cast(method->methodPointer); - } - } - return static_cast(nullptr); - }(); - static auto File_ReadAllBytes = [] { - const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); - const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; - return method ? reinterpret_cast(method->methodPointer) : nullptr; - }(); - - if (!ImageConversion_LoadImage || !File_ReadAllBytes) { - Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found."); - return false; - } - - const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); - if (!fileBytes) return false; - - if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) { - Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str()); - return false; - } - - SetDontUnloadUnusedAsset(texture2D); - AppliedLocalTextureKeys.emplace(std::move(cacheKey)); - Log::InfoFmt("Texture replaced in-place from local file: %s", path.string().c_str()); - return true; - } - - bool ApplyLocalImageToTexture2DFromCandidates(void* texture2D, const std::vector& candidates) { - for (const auto& candidate : candidates) { - if (ApplyLocalImageToTexture2D(texture2D, candidate)) { - return true; - } - } - return false; - } - - bool ReplaceSpriteTextureInPlace(void* sprite, const std::vector& candidates) { - if (!sprite || !Sprite_get_texture_Orig) return false; - - const auto texture = Sprite_get_texture_Orig(sprite); - if (!IsTexture2D(texture)) return false; - - return ApplyLocalImageToTexture2DFromCandidates(texture, candidates); - } - - void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName) { - if (!Config::replaceTexture && !Config::dumpRuntimeTexture) return result; - - if (Config::dumpRuntimeTexture) { - DumpTextureOrSpriteAsset(result); - } - if (!Config::replaceTexture) return result; - - if (IsSprite(result)) { - if (ReplaceSpriteTextureInPlace(result, GetSpriteAssetTextureCandidates(result, assetName))) { - return result; - } - return result; - } - - if (result && !IsTexture2D(result)) return result; - - if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(std::filesystem::path(assetName)))) { - return localTexture; - } - return result; - } - - void* ReplaceTextureOrSpriteByObjectName(void* result) { - if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !result) return result; - - const auto objectName = GetObjectName(result); - if (!objectName) return result; - - const auto assetPath = std::filesystem::path(objectName->ToString()); - if (Config::dumpRuntimeTexture) { - DumpTextureOrSpriteAsset(result); - } - if (!Config::replaceTexture) return result; - - if (IsSprite(result)) { - std::vector candidates; - AppendTextureCandidates(candidates, GetSpriteTextureCandidates(objectName->ToString())); - if (ReplaceSpriteTextureInPlace(result, candidates)) { - return result; - } - return result; - } - - if (IsTexture2D(result)) { - if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(assetPath))) { - return localTexture; - } - } - - return result; - } - - void ReplaceAllAssetTextures(void* allAssets) { - if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !allAssets) return; - - auto assets = reinterpret_cast*>(allAssets); - for (std::uintptr_t i = 0; i < assets->max_length; ++i) { - auto asset = assets->At(static_cast(i)); - auto replacedAsset = ReplaceTextureOrSpriteByObjectName(asset); - if (replacedAsset != asset) { - assets->At(static_cast(i)) = replacedAsset; - } - } - } - - void* ReplaceSpriteAssetByTextureName(void* sprite) { - if (!Config::replaceTexture || !sprite) return sprite; - - if (!IsSprite(sprite)) { - return sprite; - } - - if (ReplaceSpriteTextureInPlace(sprite, GetSpriteAssetTextureCandidates(sprite, ""))) { - return sprite; - } - return sprite; - } - - void* ReplaceSpriteTexture(void* texture2D) { - if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !IsTexture2D(texture2D)) return texture2D; - - const auto objectName = GetObjectName(texture2D); - if (!objectName) return texture2D; - - if (Config::dumpRuntimeTexture) { - DumpTexture2D(texture2D); - } - if (!Config::replaceTexture) return texture2D; - - if (ApplyLocalImageToTexture2DFromCandidates(texture2D, GetSpriteTextureCandidates(objectName->ToString()))) { - return texture2D; - } - return texture2D; - } - - void* ResolveSpriteGetTextureHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) { - return addr; - } - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture()")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite", "get_texture"); - } - - void* ResolveAssetBundleLoadAssetHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", - "LoadAsset_Internal", {"System.String", "System.Type"}); - } - - void* ResolveAssetBundleLoadAssetAsyncHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", - "LoadAssetAsync_Internal", {"System.String", "System.Type"}); - } - - void* ResolveAssetBundleRequestResultHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::GetResult()")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "GetResult"); - } - - void* ResolveAssetBundleRequestAssetHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_asset()")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_asset"); - } - - void* ResolveAssetBundleRequestAllAssetsHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_allAssets()")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_allAssets"); - } - - void* ResolveResourcesLoadHookAddress() { - if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( - "UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")) { - return addr; - } - return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "ResourcesAPIInternal", - "Load", {"System.String", "System.Type"}); - } - std::unordered_set updatedFontPtrs{}; void UpdateFont(void* TMP_Textself) { if (!Config::replaceFont) return; diff --git a/src/GakumasLocalify/HookTexture.cpp b/src/GakumasLocalify/HookTexture.cpp new file mode 100644 index 0000000..5d416dc --- /dev/null +++ b/src/GakumasLocalify/HookTexture.cpp @@ -0,0 +1,797 @@ +#include "HookTexture.h" + +#include "Log.h" +#include "Il2cppUtils.hpp" +#include "Local.h" +#include "config/Config.hpp" +#include "../deps/UnityResolve/UnityResolve.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GakumasLocal::HookMain +{ + using Il2cppString = UnityResolve::UnityType::String; + + extern void* (*Sprite_get_texture_Orig)(void* self); + + bool IsNativeObjectAlive(void* obj); + + Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr; + Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr; + std::unordered_map LoadedLocalTextureHandles{}; + std::unordered_set AppliedLocalTextureKeys{}; + + Il2cppUtils::Il2CppClassHead* GetTexture2DClass() { + if (!Texture2DClass) { + const auto textureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Texture2D"); + if (textureClass) { + Texture2DClass = static_cast(textureClass->address); + } + } + return Texture2DClass; + } + + Il2cppUtils::Il2CppClassHead* GetSpriteClass() { + if (!SpriteClass) { + const auto spriteClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite"); + if (spriteClass) { + SpriteClass = static_cast(spriteClass->address); + } + } + return SpriteClass; + } + + bool IsTexture2D(void* obj) { + const auto textureClass = GetTexture2DClass(); + if (!obj || !textureClass) return false; + + const auto objClass = Il2cppUtils::get_class_from_instance(obj); + if (objClass == textureClass) return true; + + return UnityResolve::Invoke("il2cpp_class_is_assignable_from", textureClass, objClass); + } + + bool IsSprite(void* obj) { + const auto spriteClass = GetSpriteClass(); + if (!obj || !spriteClass) return false; + + const auto objClass = Il2cppUtils::get_class_from_instance(obj); + if (objClass == spriteClass) return true; + + return UnityResolve::Invoke("il2cpp_class_is_assignable_from", spriteClass, objClass); + } + + Il2cppString* GetObjectName(void* obj) { + if (!obj) return nullptr; + + static auto Object_GetName = reinterpret_cast( + Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)")); + return Object_GetName ? Object_GetName(obj) : nullptr; + } + + void SetDontUnloadUnusedAsset(void* obj) { + if (!obj) return; + + static auto Object_set_hideFlags = reinterpret_cast( + Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Object", "set_hideFlags")); + if (Object_set_hideFlags) { + Object_set_hideFlags(obj, 32); + } + } + + void AddTexturePathCandidate(std::vector& candidates, const std::filesystem::path& path) { + if (path.empty()) return; + if (std::find(candidates.begin(), candidates.end(), path) == candidates.end()) { + candidates.emplace_back(path); + } + if (!path.has_extension()) { + auto pngPath = path; + pngPath += ".png"; + if (std::find(candidates.begin(), candidates.end(), pngPath) == candidates.end()) { + candidates.emplace_back(std::move(pngPath)); + } + } + } + + enum class TextureCategory { + Image, + Atlas, + Others, + }; + + std::string ToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; + } + + TextureCategory GetTextureCategory(const std::string& textureName) { + const auto lowerName = ToLowerAscii(std::filesystem::path(textureName).filename().generic_string()); + if (lowerName.rfind("img", 0) == 0) { + return TextureCategory::Image; + } + if (lowerName.rfind("sactx", 0) == 0) { + return TextureCategory::Atlas; + } + return TextureCategory::Others; + } + + std::filesystem::path GetTextureCategoryDirName(TextureCategory category) { + switch (category) { + case TextureCategory::Image: + return "image"; + case TextureCategory::Atlas: + return "atlas"; + default: + return "others"; + } + } + + std::filesystem::path GetTextureReplaceRoot() { + return Local::GetBasePath() / "texture2d"; + } + + std::filesystem::path GetTextureDumpRoot() { + return Local::GetBasePath() / "dump-files" / "texture2d"; + } + + std::filesystem::path GetTextureReplaceBase(const std::string& textureName) { + return GetTextureReplaceRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); + } + + std::filesystem::path GetTextureDumpBase(const std::string& textureName) { + return GetTextureDumpRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName)); + } + + std::vector SplitString(const std::string& value, char delimiter) { + std::vector parts; + size_t start = 0; + while (start <= value.size()) { + const auto end = value.find(delimiter, start); + parts.emplace_back(value.substr(start, end == std::string::npos ? std::string::npos : end - start)); + if (end == std::string::npos) break; + start = end + 1; + } + return parts; + } + + void AppendTextureCandidates(std::vector& target, std::vector&& source); + std::string NormalizeLocalAssetKey(const std::filesystem::path& path); + + bool IsHexHashPart(const std::string& value) { + return value.size() == 8 && std::all_of(value.begin(), value.end(), [](unsigned char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); + }); + } + + std::string GetPortableSactxTextureName(const std::string& objectName) { + auto fileName = std::filesystem::path(objectName).filename().generic_string(); + if (fileName.ends_with(".png")) { + fileName.resize(fileName.size() - 4); + } + + const auto parts = SplitString(fileName, '-'); + if (parts.size() < 5 || parts[0] != "sactx" || parts[2].find('x') == std::string::npos) { + return {}; + } + + const auto atlasEnd = IsHexHashPart(parts.back()) ? parts.size() - 1 : parts.size(); + if (atlasEnd <= 4) return {}; + + std::string portableName = parts[0] + "-" + parts[1] + "-" + parts[2]; + for (size_t i = 4; i < atlasEnd; ++i) { + portableName += "-" + parts[i]; + } + return portableName; + } + + std::unordered_map>> RecursiveTexturePathIndex{}; + + std::vector GetRecursiveTextureCandidates(const std::filesystem::path& basePath, + const std::string& lookupName) { + std::vector candidates; + if (lookupName.empty() || !std::filesystem::exists(basePath)) return candidates; + + const auto baseKey = NormalizeLocalAssetKey(basePath); + auto& index = RecursiveTexturePathIndex[baseKey]; + if (index.empty()) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(basePath)) { + if (!entry.is_regular_file()) continue; + + const auto& path = entry.path(); + if (ToLowerAscii(path.extension().generic_string()) != ".png") continue; + + const auto fileName = path.filename().generic_string(); + const auto stemName = path.stem().generic_string(); + index[fileName].emplace_back(path); + if (stemName != fileName) { + index[stemName].emplace_back(path); + } + } + } + + if (const auto iter = index.find(lookupName); iter != index.end()) { + candidates.insert(candidates.end(), iter->second.begin(), iter->second.end()); + } + return candidates; + } + + std::vector GetNamedTextureCandidates(const std::filesystem::path& assetName) { + std::vector candidates; + if (assetName.empty()) return candidates; + + const auto basePath = GetTextureReplaceBase(assetName.filename().generic_string()); + AddTexturePathCandidate(candidates, basePath / assetName); + if (assetName.has_parent_path()) { + AddTexturePathCandidate(candidates, basePath / assetName.filename()); + } + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, assetName.filename().generic_string())); + + const auto portableAssetName = GetPortableSactxTextureName(assetName.filename().generic_string()); + if (!portableAssetName.empty()) { + AddTexturePathCandidate(candidates, basePath / portableAssetName); + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableAssetName)); + } + return candidates; + } + + std::vector GetSpriteTextureCandidates(const std::string& objectName) { + std::vector candidates; + if (objectName.empty()) return candidates; + + auto safeObjectName = objectName; + std::replace(safeObjectName.begin(), safeObjectName.end(), '|', '_'); + + const auto basePath = GetTextureReplaceBase(safeObjectName); + AddTexturePathCandidate(candidates, basePath / objectName); + if (safeObjectName != objectName) { + AddTexturePathCandidate(candidates, basePath / safeObjectName); + } + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, safeObjectName)); + const auto portableObjectName = GetPortableSactxTextureName(safeObjectName); + if (!portableObjectName.empty() && portableObjectName != objectName && portableObjectName != safeObjectName) { + AddTexturePathCandidate(candidates, basePath / portableObjectName); + AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableObjectName)); + } + return candidates; + } + + void AppendTextureCandidates(std::vector& target, std::vector&& source) { + target.insert(target.end(), + std::make_move_iterator(source.begin()), + std::make_move_iterator(source.end())); + } + + std::vector GetSpriteAssetTextureCandidates(void* sprite, const std::string& assetName) { + std::vector candidates; + + const auto assetPath = std::filesystem::path(assetName); + if (!assetName.empty()) { + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(assetPath.filename().generic_string())); + } + + if (sprite && Sprite_get_texture_Orig) { + if (const auto texture = Sprite_get_texture_Orig(sprite)) { + if (const auto textureName = GetObjectName(texture)) { + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(textureName->ToString())); + } + } + } + return candidates; + } + + std::string NormalizeLocalAssetKey(const std::filesystem::path& path) { + auto key = path.lexically_normal().generic_string(); + std::replace(key.begin(), key.end(), '\\', '/'); + return key; + } + + std::string SanitizeDumpPathPart(std::string part) { + constexpr std::string_view invalidChars = "<>:\"/\\|?*"; + if (part.empty() || part == "." || part == "..") return "_"; + + for (auto& ch : part) { + if (static_cast(ch) < 32 || invalidChars.find(ch) != std::string_view::npos) { + ch = '_'; + } + } + + while (!part.empty() && (part.back() == '.' || part.back() == ' ')) { + part.back() = '_'; + } + return part.empty() ? "_" : part; + } + + std::filesystem::path SanitizeDumpSubPath(const std::filesystem::path& dumpSubDir) { + std::filesystem::path safePath; + for (const auto& part : dumpSubDir) { + const auto partString = part.generic_string(); + if (partString.empty() || partString == "." || partString == ".." + || part == part.root_name() || part == part.root_directory()) { + continue; + } + safePath /= SanitizeDumpPathPart(partString); + } + return safePath; + } + + bool DumpTexture2D(void* texture2D) { + if (!IsTexture2D(texture2D)) return false; + + const auto objectName = GetObjectName(texture2D); + const auto textureName = objectName ? objectName->ToString() : std::string("texture"); + const auto dumpDir = GetTextureDumpBase(textureName); + const auto dumpPath = dumpDir / (SanitizeDumpPathPart(textureName) + ".png"); + + if (std::filesystem::exists(dumpPath)) return true; + + static auto Texture2D_get_width = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_width", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_get_height = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_height", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_ctor = [] { + const auto textureClass = GetTexture2DClass(); + const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; + return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; + }(); + static auto Texture2D_ReadPixels = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "ReadPixels", 3) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Texture2D_Apply = [] { + const auto textureClass = GetTexture2DClass(); + const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "Apply", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_GetTemporary = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "GetTemporary", 3) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_ReleaseTemporary = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "ReleaseTemporary", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_get_active = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "get_active", 0) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto RenderTexture_set_active = [] { + const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture"); + const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "set_active", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto Graphics_Blit = [] { + const auto graphicsClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Graphics"); + const auto method = graphicsClass ? Il2cppUtils::il2cpp_class_get_method_from_name(graphicsClass->address, "Blit", 2) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + static auto ImageConversion_EncodeToPNG = [] { + using EncodeToPNGFn = void* (*)(void*); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.ImageConversion::EncodeToPNG(UnityEngine.Texture2D)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "EncodeToPNG", 1) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_WriteAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "WriteAllBytes", 2) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!Texture2D_get_width || !Texture2D_get_height || !Texture2D_ctor || !Texture2D_ReadPixels + || !Texture2D_Apply || !RenderTexture_GetTemporary || !RenderTexture_ReleaseTemporary + || !RenderTexture_get_active || !RenderTexture_set_active || !Graphics_Blit + || !ImageConversion_EncodeToPNG || !File_WriteAllBytes) { + Log::Error("DumpTexture2D failed: Unity texture dump API not found."); + return false; + } + + const auto width = Texture2D_get_width(texture2D); + const auto height = Texture2D_get_height(texture2D); + if (width <= 0 || height <= 0) return false; + + void* renderTexture = nullptr; + void* readableTexture = nullptr; + void* previousActive = nullptr; + const auto cleanup = [&] { + if (RenderTexture_get_active && RenderTexture_set_active + && (previousActive || RenderTexture_get_active() == renderTexture)) { + RenderTexture_set_active(previousActive); + } + if (renderTexture && RenderTexture_ReleaseTemporary) { + RenderTexture_ReleaseTemporary(renderTexture); + } + }; + + try { + std::filesystem::create_directories(dumpDir); + + renderTexture = RenderTexture_GetTemporary(width, height, 0); + if (!renderTexture) { + cleanup(); + return false; + } + + Graphics_Blit(texture2D, renderTexture); + previousActive = RenderTexture_get_active(); + RenderTexture_set_active(renderTexture); + + readableTexture = UnityResolve::Invoke("il2cpp_object_new", GetTexture2DClass()); + if (!readableTexture) { + cleanup(); + return false; + } + + Texture2D_ctor(readableTexture, width, height); + Texture2D_ReadPixels(readableTexture, UnityResolve::UnityType::Rect(0, 0, static_cast(width), static_cast(height)), 0, 0); + Texture2D_Apply(readableTexture); + + const auto pngBytes = ImageConversion_EncodeToPNG(readableTexture); + if (!pngBytes) { + cleanup(); + return false; + } + + File_WriteAllBytes(Il2cppString::New(dumpPath.string()), pngBytes); + Log::InfoFmt("Texture dumped: %s", dumpPath.string().c_str()); + cleanup(); + return true; + } + catch (const std::exception& ex) { + cleanup(); + Log::ErrorFmt("DumpTexture2D failed: %s", ex.what()); + return false; + } + catch (...) { + cleanup(); + Log::Error("DumpTexture2D failed: unknown error."); + return false; + } + } + + void DumpTextureOrSpriteAsset(void* result) { + if (!result) return; + + if (IsTexture2D(result)) { + DumpTexture2D(result); + return; + } + if (IsSprite(result) && Sprite_get_texture_Orig) { + if (const auto texture = Sprite_get_texture_Orig(result)) { + DumpTexture2D(texture); + } + } + } + + void* LoadLocalTexture2D(const std::filesystem::path& path) { + if (!std::filesystem::is_regular_file(path)) return nullptr; + + const auto cacheKey = NormalizeLocalAssetKey(path); + if (const auto iter = LoadedLocalTextureHandles.find(cacheKey); iter != LoadedLocalTextureHandles.end()) { + const auto cachedTexture = UnityResolve::Invoke("il2cpp_gchandle_get_target", iter->second); + if (cachedTexture && IsNativeObjectAlive(cachedTexture)) { + return cachedTexture; + } + + UnityResolve::Invoke("il2cpp_gchandle_free", iter->second); + LoadedLocalTextureHandles.erase(iter); + } + + const auto textureClass = GetTexture2DClass(); + if (!textureClass) return nullptr; + + static auto Texture2D_ctor = [] { + const auto textureClass = GetTexture2DClass(); + const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr; + return ctor ? reinterpret_cast(ctor->methodPointer) : nullptr; + }(); + static auto ImageConversion_LoadImage = [] { + using LoadImageFn = bool (*)(void*, void*, bool); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_ReadAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) { + Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found."); + return nullptr; + } + + const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); + if (!fileBytes) return nullptr; + + const auto texture = UnityResolve::Invoke("il2cpp_object_new", textureClass); + Texture2D_ctor(texture, 2, 2); + if (!ImageConversion_LoadImage(texture, fileBytes, false)) { + Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str()); + return nullptr; + } + + SetDontUnloadUnusedAsset(texture); + LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke("il2cpp_gchandle_new", texture, false)); + Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str()); + return texture; + } + + void* LoadLocalTexture2DFromCandidates(const std::vector& candidates) { + for (const auto& candidate : candidates) { + if (auto texture = LoadLocalTexture2D(candidate)) { + return texture; + } + } + return nullptr; + } + + bool ApplyLocalImageToTexture2D(void* texture2D, const std::filesystem::path& path) { + if (!IsTexture2D(texture2D) || !std::filesystem::is_regular_file(path)) return false; + + auto cacheKey = NormalizeLocalAssetKey(path) + + "|" + std::to_string(reinterpret_cast(texture2D)); + if (AppliedLocalTextureKeys.contains(cacheKey)) return true; + + static auto ImageConversion_LoadImage = [] { + using LoadImageFn = bool (*)(void*, void*, bool); + if (const auto icall = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) { + return reinterpret_cast(icall); + } + + for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) { + const auto assembly = UnityResolve::Get(assemblyName); + const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr; + const auto method = imageConversionClass + ? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3) + : nullptr; + if (method) { + return reinterpret_cast(method->methodPointer); + } + } + return static_cast(nullptr); + }(); + static auto File_ReadAllBytes = [] { + const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File"); + const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr; + return method ? reinterpret_cast(method->methodPointer) : nullptr; + }(); + + if (!ImageConversion_LoadImage || !File_ReadAllBytes) { + Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found."); + return false; + } + + const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string())); + if (!fileBytes) return false; + + if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) { + Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str()); + return false; + } + + SetDontUnloadUnusedAsset(texture2D); + AppliedLocalTextureKeys.emplace(std::move(cacheKey)); + Log::InfoFmt("Texture replaced in-place from local file: %s", path.string().c_str()); + return true; + } + + bool ApplyLocalImageToTexture2DFromCandidates(void* texture2D, const std::vector& candidates) { + for (const auto& candidate : candidates) { + if (ApplyLocalImageToTexture2D(texture2D, candidate)) { + return true; + } + } + return false; + } + + bool ReplaceSpriteTextureInPlace(void* sprite, const std::vector& candidates) { + if (!sprite || !Sprite_get_texture_Orig) return false; + + const auto texture = Sprite_get_texture_Orig(sprite); + if (!IsTexture2D(texture)) return false; + + return ApplyLocalImageToTexture2DFromCandidates(texture, candidates); + } + + void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName) { + if (!Config::replaceTexture && !Config::dumpRuntimeTexture) return result; + + if (Config::dumpRuntimeTexture) { + DumpTextureOrSpriteAsset(result); + } + if (!Config::replaceTexture) return result; + + if (IsSprite(result)) { + if (ReplaceSpriteTextureInPlace(result, GetSpriteAssetTextureCandidates(result, assetName))) { + return result; + } + return result; + } + + if (result && !IsTexture2D(result)) return result; + + if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(std::filesystem::path(assetName)))) { + return localTexture; + } + return result; + } + + void* ReplaceTextureOrSpriteByObjectName(void* result) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !result) return result; + + const auto objectName = GetObjectName(result); + if (!objectName) return result; + + const auto assetPath = std::filesystem::path(objectName->ToString()); + if (Config::dumpRuntimeTexture) { + DumpTextureOrSpriteAsset(result); + } + if (!Config::replaceTexture) return result; + + if (IsSprite(result)) { + std::vector candidates; + AppendTextureCandidates(candidates, GetSpriteTextureCandidates(objectName->ToString())); + if (ReplaceSpriteTextureInPlace(result, candidates)) { + return result; + } + return result; + } + + if (IsTexture2D(result)) { + if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(assetPath))) { + return localTexture; + } + } + + return result; + } + + void ReplaceAllAssetTextures(void* allAssets) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !allAssets) return; + + auto assets = reinterpret_cast*>(allAssets); + for (std::uintptr_t i = 0; i < assets->max_length; ++i) { + auto asset = assets->At(static_cast(i)); + auto replacedAsset = ReplaceTextureOrSpriteByObjectName(asset); + if (replacedAsset != asset) { + assets->At(static_cast(i)) = replacedAsset; + } + } + } + + void* ReplaceSpriteAssetByTextureName(void* sprite) { + if (!Config::replaceTexture || !sprite) return sprite; + + if (!IsSprite(sprite)) { + return sprite; + } + + if (ReplaceSpriteTextureInPlace(sprite, GetSpriteAssetTextureCandidates(sprite, ""))) { + return sprite; + } + return sprite; + } + + void* ReplaceSpriteTexture(void* texture2D) { + if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !IsTexture2D(texture2D)) return texture2D; + + const auto objectName = GetObjectName(texture2D); + if (!objectName) return texture2D; + + if (Config::dumpRuntimeTexture) { + DumpTexture2D(texture2D); + } + if (!Config::replaceTexture) return texture2D; + + if (ApplyLocalImageToTexture2DFromCandidates(texture2D, GetSpriteTextureCandidates(objectName->ToString()))) { + return texture2D; + } + return texture2D; + } + + void* ResolveSpriteGetTextureHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) { + return addr; + } + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite", "get_texture"); + } + + void* ResolveAssetBundleLoadAssetHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", + "LoadAsset_Internal", {"System.String", "System.Type"}); + } + + void* ResolveAssetBundleLoadAssetAsyncHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle", + "LoadAssetAsync_Internal", {"System.String", "System.Type"}); + } + + void* ResolveAssetBundleRequestResultHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::GetResult()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "GetResult"); + } + + void* ResolveAssetBundleRequestAssetHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_asset()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_asset"); + } + + void* ResolveAssetBundleRequestAllAssetsHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_allAssets()")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_allAssets"); + } + + void* ResolveResourcesLoadHookAddress() { + if (const auto addr = Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")) { + return addr; + } + return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "ResourcesAPIInternal", + "Load", {"System.String", "System.Type"}); + } + +} diff --git a/src/GakumasLocalify/HookTexture.h b/src/GakumasLocalify/HookTexture.h new file mode 100644 index 0000000..e5b3ded --- /dev/null +++ b/src/GakumasLocalify/HookTexture.h @@ -0,0 +1,23 @@ +#ifndef GAKUMAS_LOCALIFY_HOOK_TEXTURE_H +#define GAKUMAS_LOCALIFY_HOOK_TEXTURE_H + +#include + +namespace GakumasLocal::HookMain +{ + void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName); + void* ReplaceTextureOrSpriteByObjectName(void* result); + void ReplaceAllAssetTextures(void* allAssets); + void* ReplaceSpriteAssetByTextureName(void* sprite); + void* ReplaceSpriteTexture(void* texture2D); + + void* ResolveSpriteGetTextureHookAddress(); + void* ResolveAssetBundleLoadAssetHookAddress(); + void* ResolveAssetBundleLoadAssetAsyncHookAddress(); + void* ResolveAssetBundleRequestResultHookAddress(); + void* ResolveAssetBundleRequestAssetHookAddress(); + void* ResolveAssetBundleRequestAllAssetsHookAddress(); + void* ResolveResourcesLoadHookAddress(); +} + +#endif // GAKUMAS_LOCALIFY_HOOK_TEXTURE_H