From 36a49ba4df2cb9b94dc6d27f03cff403fea85ab2 Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sat, 9 May 2026 20:37:22 +0800 Subject: [PATCH 1/5] =?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 --- .gitignore | 2 + app/build.gradle | 19 +- app/src/main/cpp/GakumasLocalify/Hook.cpp | 857 +++++++++++++++++- .../cpp/GakumasLocalify/config/Config.cpp | 6 + .../cpp/GakumasLocalify/config/Config.hpp | 2 + .../gakumas/localify/ActivityExtends.kt | 32 +- .../gakumas/localify/ConfigUpdateListener.kt | 43 + .../gakumas/localify/GakumasHookMain.kt | 23 +- .../chinosk/gakumas/localify/MainActivity.kt | 52 +- .../localify/hookUtils/FilesChecker.kt | 9 +- .../localify/mainUtils/FileDownloader.kt | 95 ++ .../mainUtils/TextureResourceUpdater.kt | 322 +++++++ .../gakumas/localify/models/GakumasConfig.kt | 2 + .../gakumas/localify/models/ProgramConfig.kt | 3 + .../gakumas/localify/models/ViewModels.kt | 20 +- .../gakumas/localify/ui/pages/MainPage.kt | 5 +- .../ui/pages/subPages/AdvancedSettingsPage.kt | 4 + .../localify/ui/pages/subPages/HomePage.kt | 229 ++++- app/src/main/res/values-ja/strings.xml | 7 + app/src/main/res/values-zh-rCN/strings.xml | 9 +- app/src/main/res/values-zh-rHK/strings.xml | 113 +++ app/src/main/res/values-zh-rMO/strings.xml | 113 +++ app/src/main/res/values-zh-rTW/strings.xml | 113 +++ app/src/main/res/values/strings.xml | 9 +- 24 files changed, 2059 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/TextureResourceUpdater.kt create mode 100644 app/src/main/res/values-zh-rHK/strings.xml create mode 100644 app/src/main/res/values-zh-rMO/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml diff --git a/.gitignore b/.gitignore index a9fd16a..455dbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ local.properties /.kotlin /app/debug /app/release + +app/src/main/assets/gakumas-local diff --git a/app/build.gradle b/app/build.gradle index 256e6b1..75145c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,19 @@ plugins { id("kotlin-parcelize") } +def sharedVersionFile = file("$projectDir/src/main/cpp/GakumasLocalify/VERSION") +if (!sharedVersionFile.exists()) { + throw new GradleException("Shared version file not found: ${sharedVersionFile}") +} +def sharedVersionName = sharedVersionFile.text.trim() +def sharedVersionMatcher = sharedVersionName =~ /^v?(\d+)\.(\d+)\.(\d+).*$/ +if (!sharedVersionMatcher.matches()) { + throw new GradleException("Invalid shared version: ${sharedVersionName}") +} +def sharedVersionCode = sharedVersionMatcher[0][1].toInteger() * 10000 + + sharedVersionMatcher[0][2].toInteger() * 100 + + sharedVersionMatcher[0][3].toInteger() + android { namespace 'io.github.chinosk.gakumas.localify' compileSdk 34 @@ -15,8 +28,8 @@ android { applicationId "io.github.chinosk.gakumas.localify" minSdk 29 targetSdk 34 - versionCode 12 - versionName "v3.2.0" + versionCode sharedVersionCode + versionName sharedVersionName buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -132,4 +145,4 @@ dependencies { implementation(libs.shadowhook) compileOnly(libs.xposed.api) implementation(libs.kotlinx.serialization.json) -} \ No newline at end of file +} diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp index dd5290e..b197d67 100644 --- a/app/src/main/cpp/GakumasLocalify/Hook.cpp +++ b/app/src/main/cpp/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/app/src/main/cpp/GakumasLocalify/config/Config.cpp b/app/src/main/cpp/GakumasLocalify/config/Config.cpp index 57ea8e3..f1bacea 100644 --- a/app/src/main/cpp/GakumasLocalify/config/Config.cpp +++ b/app/src/main/cpp/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/app/src/main/cpp/GakumasLocalify/config/Config.hpp b/app/src/main/cpp/GakumasLocalify/config/Config.hpp index 1147f53..348f0e3 100644 --- a/app/src/main/cpp/GakumasLocalify/config/Config.hpp +++ b/app/src/main/cpp/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/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt index 74180cd..9d79450 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ActivityExtends.kt @@ -2,8 +2,10 @@ package io.github.chinosk.gakumas.localify import android.app.Activity import android.content.Intent +import android.util.Log import android.widget.Toast import androidx.core.content.FileProvider +import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater import io.github.chinosk.gakumas.localify.mainUtils.json import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.ProgramConfig @@ -77,6 +79,9 @@ fun T.loadConfig() where T : Activity, T : IHasConfigItems { if (programConfig.useAPIAssetsURL.isEmpty()) { programConfig.useAPIAssetsURL = getString(R.string.default_assets_check_api) } + if (programConfig.useAPITextureAssetsURL.isEmpty()) { + programConfig.useAPITextureAssetsURL = getString(R.string.default_texture_assets_check_api) + } } fun T.onClickStartGame() where T : Activity, T : IHasConfigItems { @@ -105,7 +110,7 @@ fun T.onClickStartGame() where T : Activity, T : IHasConfigItems { putExtra( "localData", getProgramConfigContent(listOf("transRemoteZipUrl", "useAPIAssetsURL", - "localAPIAssetsVersion", "p"), programConfig) + "useAPITextureAssetsURL", "localAPIAssetsVersion", "p"), programConfig) ) putExtra("lVerName", version) flags = Intent.FLAG_ACTIVITY_NEW_TASK @@ -141,5 +146,30 @@ fun T.onClickStartGame() where T : Activity, T : IHasConfigItems { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } + val textureUpdateFile = TextureResourceUpdater.getCachedZipFile(this) + Log.i(TAG, "Texture cache before launch: replaceTexture=${config.replaceTexture}, " + + "useAPITextureAssets=${programConfig.useAPITextureAssets}, " + + "exists=${textureUpdateFile.exists()}, size=${if (textureUpdateFile.exists()) textureUpdateFile.length() else 0}, " + + "path=${textureUpdateFile.absolutePath}") + if (config.replaceTexture && textureUpdateFile.exists()) { + val textureUri = FileProvider.getUriForFile( + this, + "io.github.chinosk.gakumas.localify.fileprovider", + textureUpdateFile + ) + + grantUriPermission( + "com.bandainamcoent.idolmaster_gakuen", + textureUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + intent.putExtra("texture_resource_file", textureUri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + Log.i(TAG, "Texture resource uri attached: $textureUri") + } + else { + Log.i(TAG, "Texture resource uri not attached.") + } + startActivity(intent) } diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt index e1ed009..67a05c0 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ConfigUpdateListener.kt @@ -21,6 +21,7 @@ interface ConfigListener { fun onTextTestChanged(value: Boolean) fun onUseMasterTransChanged(value: Boolean) fun onReplaceFontChanged(value: Boolean) + fun onReplaceTextureChanged(value: Boolean) fun onLazyInitChanged(value: Boolean) fun onEnableFreeCameraChanged(value: Boolean) fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int) @@ -39,6 +40,7 @@ interface ConfigListener { fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) fun onGameOrientationChanged(checkedId: Int) fun onDumpTextChanged(value: Boolean) + fun onDumpRuntimeTextureChanged(value: Boolean) fun onEnableBreastParamChanged(value: Boolean) fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int) @@ -69,8 +71,15 @@ interface ConfigListener { localResourceVersionState: String? = null, errorString: String? = null, localAPIResourceVersion: String? = null) + fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean? = null, + downloadProgressState: Float? = null, + localTextureResourceVersion: String? = null, + errorString: String? = null) fun onPUseAPIAssetsChanged(value: Boolean) fun onPUseAPIAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) + fun onPUseAPITextureAssetsChanged(value: Boolean) + fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) + fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean) fun mainUIConfirmStatUpdate(isShow: Boolean? = null, title: String? = null, content: String? = null, onConfirm: (() -> Unit)? = { mainUIConfirmStatUpdate(isShow = false) }, @@ -129,6 +138,12 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems { pushKeyEvent(KeyEvent(1145, 30)) } + override fun onReplaceTextureChanged(value: Boolean) { + config.replaceTexture = value + saveConfig() + pushKeyEvent(KeyEvent(1145, 30)) + } + override fun onLazyInitChanged(value: Boolean) { config.lazyInit = value saveConfig() @@ -149,6 +164,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems { saveConfig() } + override fun onDumpRuntimeTextureChanged(value: Boolean) { + config.dumpRuntimeTexture = value + saveConfig() + } + override fun onEnableFreeCameraChanged(value: Boolean) { config.enableFreeCamera = value saveConfig() @@ -576,6 +596,14 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems { localAPIResourceVersion?.let{ programConfigViewModel.localAPIResourceVersionState.value = it } } + override fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?, + localTextureResourceVersion: String?, errorString: String?) { + downloadAbleState?.let { programConfigViewModel.textureDownloadAbleState.value = it } + downloadProgressState?.let{ programConfigViewModel.textureDownloadProgressState.value = it } + localTextureResourceVersion?.let{ programConfigViewModel.localTextureResourceVersionState.value = it } + errorString?.let{ programConfigViewModel.textureErrorStringState.value = it } + } + override fun onPUseAPIAssetsChanged(value: Boolean) { programConfig.useAPIAssets = value if (value) { @@ -591,6 +619,21 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems { saveProgramConfig() } + override fun onPUseAPITextureAssetsChanged(value: Boolean) { + programConfig.useAPITextureAssets = value + saveProgramConfig() + } + + override fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) { + programConfig.useAPITextureAssetsURL = s.toString() + saveProgramConfig() + } + + override fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean) { + programConfig.delTextureRemoteAfterUpdate = value + saveProgramConfig() + } + override fun mainUIConfirmStatUpdate(isShow: Boolean?, title: String?, content: String?, onConfirm: (() -> Unit)?, onCancel: (() -> Unit)? ) { diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt index 55891f2..a199599 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt @@ -35,6 +35,7 @@ import java.util.Locale import kotlin.system.measureTimeMillis import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir +import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater import io.github.chinosk.gakumas.localify.mainUtils.json import io.github.chinosk.gakumas.localify.models.NativeInitProgress import io.github.chinosk.gakumas.localify.models.ProgramConfig @@ -53,6 +54,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { private var getConfigError: Exception? = null private var externalFilesChecked: Boolean = false + private var textureFilesChecked: Boolean = false private var gameActivity: Activity? = null override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { @@ -311,6 +313,25 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { } } + if (initConfig?.replaceTexture == true && !textureFilesChecked) { + val textureDataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("texture_resource_file", Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra("texture_resource_file") + } + + if (textureDataUri != null) { + Log.i(TAG, "Texture resource uri received: $textureDataUri") + textureFilesChecked = true + TextureResourceUpdater.updateTextureFilesFromZip(activity, textureDataUri, + activity.filesDir, programConfig?.delTextureRemoteAfterUpdate ?: true) + } + else { + Log.i(TAG, "Texture resource uri missing.") + } + } + loadConfig(gkmsData) Log.d(TAG, "gkmsData: $gkmsData") } @@ -509,4 +530,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt index 1740d76..faf1cda 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt @@ -18,6 +18,7 @@ import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker import io.github.chinosk.gakumas.localify.mainUtils.ShizukuApi +import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater import io.github.chinosk.gakumas.localify.mainUtils.json import io.github.chinosk.gakumas.localify.models.ConfirmStateModel import io.github.chinosk.gakumas.localify.models.GakumasConfig @@ -79,6 +80,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct fun getVersion(): List { var versionText = "" var resVersionText = "unknown" + var textureVersionText = "unknown" try { val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt") @@ -87,6 +89,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct if (programConfig.useAPIAssets) { RemoteAPIFilesChecker.getLocalVersion(this)?.let { resVersionText = it } } + TextureResourceUpdater.getLocalVersion(this)?.let { textureVersionText = it } val packInfo = packageManager.getPackageInfo(packageName, 0) val version = packInfo.versionName @@ -95,7 +98,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct } catch (_: Exception) {} - return listOf(versionText, resVersionText) + return listOf(versionText, resVersionText, textureVersionText) } fun openUrl(url: String) { @@ -130,7 +133,8 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java] programConfigFactory = ProgramConfigViewModelFactory(programConfig, - FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString() + FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString(), + TextureResourceUpdater.getLocalVersion(this).toString() ) programConfigViewModel = ViewModelProvider(this, programConfigFactory)[ProgramConfigViewModel::class.java] @@ -222,6 +226,50 @@ fun getProgramDownloadErrorStringState(context: MainActivity?): State { } } +@Composable +fun getProgramTextureDownloadState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.textureDownloadProgress.collectAsState() + } + else { + val configMSF = MutableStateFlow(0f) + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramTextureDownloadAbleState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.textureDownloadAble.collectAsState() + } + else { + val configMSF = MutableStateFlow(true) + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramLocalTextureResourceVersionState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.localTextureResourceVersion.collectAsState() + } + else { + val configMSF = MutableStateFlow("null") + configMSF.asStateFlow().collectAsState() + } +} + +@Composable +fun getProgramTextureDownloadErrorStringState(context: MainActivity?): State { + return if (context != null) { + context.programConfigViewModel.textureErrorString.collectAsState() + } + else { + val configMSF = MutableStateFlow("") + configMSF.asStateFlow().collectAsState() + } +} + @Composable fun getMainUIConfirmState(context: MainActivity?, previewData: ConfirmStateModel? = null): State { return if (context != null) { diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt index ccae7d7..71e526a 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/hookUtils/FilesChecker.kt @@ -45,6 +45,7 @@ object FilesChecker { if (!pluginBasePath.exists()) { pluginBasePath.mkdirs() } + val skipBuiltInTexture2d = File(filesDir, "$localizationFilesDir/texture2d").exists() val assets = XModuleResources.createInstance(modulePath, null).assets fun forAllAssetFiles( @@ -65,6 +66,12 @@ object FilesChecker { } } forAllAssetFiles(localizationFilesDir) { path, file -> + if ((path == "$localizationFilesDir/texture2d" || + path.startsWith("$localizationFilesDir/texture2d/")) && + skipBuiltInTexture2d) { + return@forAllAssetFiles + } + val outFile = File(filesDir, path) if (file == null) { outFile.mkdirs() @@ -167,4 +174,4 @@ object FilesChecker { i18nFile.writeText("{}") } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt index f115161..1f02f53 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/FileDownloader.kt @@ -3,6 +3,8 @@ package io.github.chinosk.gakumas.localify.mainUtils import okhttp3.* import java.io.IOException import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream import java.util.concurrent.TimeUnit object FileDownloader { @@ -111,6 +113,99 @@ object FileDownloader { } + fun downloadFileToPath( + url: String, + outputFile: File, + onDownload: (Float, downloaded: Long, size: Long) -> Unit, + onSuccess: (File) -> Unit, + onFailed: (Int, String) -> Unit, + checkContentTypes: List? = null + ) { + try { + if (call != null) { + onFailed(-1, "Another file is downloading.") + return + } + outputFile.parentFile?.mkdirs() + + val request = Request.Builder() + .url(url) + .build() + + call = client.newCall(request) + call?.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + this@FileDownloader.call = null + if (call.isCanceled()) { + onFailed(-1, "Download canceled") + } else { + onFailed(-1, e.message ?: "Unknown error") + } + } + + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + this@FileDownloader.call = null + onFailed(response.code, response.message) + return + } + + if (checkContentTypes != null) { + val contentType = response.header("Content-Type") + if (!checkContentTypes.contains(contentType)) { + onFailed(-1, "Unexpected content type: $contentType") + this@FileDownloader.call = null + return + } + } + + response.body?.let { responseBody -> + val contentLength = responseBody.contentLength() + val inputStream = responseBody.byteStream() + val buffer = ByteArray(8 * 1024) + var downloadedBytes = 0L + var read: Int + + try { + FileOutputStream(outputFile).use { outputStream -> + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + downloadedBytes += read + val progress = if (contentLength < 0) { + 0f + } + else { + downloadedBytes.toFloat() / contentLength + } + onDownload(progress, downloadedBytes, contentLength) + } + outputStream.flush() + } + onSuccess(outputFile) + } catch (e: IOException) { + outputFile.delete() + if (call.isCanceled()) { + onFailed(-1, "Download canceled") + } else { + onFailed(-1, e.message ?: "Error reading stream") + } + } finally { + this@FileDownloader.call = null + inputStream.close() + } + } ?: run { + this@FileDownloader.call = null + onFailed(-1, "Response body is null") + } + } + }) + } + catch (e: Exception) { + onFailed(-1, e.toString()) + call = null + } + } + fun cancel() { call?.cancel() this@FileDownloader.call = null diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/TextureResourceUpdater.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/TextureResourceUpdater.kt new file mode 100644 index 0000000..c1df90c --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/mainUtils/TextureResourceUpdater.kt @@ -0,0 +1,322 @@ +package io.github.chinosk.gakumas.localify.mainUtils + +import android.app.Activity +import android.content.Context +import android.net.Uri +import android.util.Log +import io.github.chinosk.gakumas.localify.GakumasHookMain +import io.github.chinosk.gakumas.localify.TAG +import io.github.chinosk.gakumas.localify.models.Asset +import io.github.chinosk.gakumas.localify.models.GithubReleaseModel +import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Request +import okhttp3.Response +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipFile + +object TextureResourceUpdater { + private const val CACHE_DIR = "remote_texture_files" + private const val CACHE_ZIP_NAME = "texture.zip" + private const val CACHE_VERSION_NAME = "texture_version.txt" + private const val TEXTURE_DIR = "gakumas-local/texture2d" + private const val VERSION_FILE_NAME = "texture_version.txt" + + private data class TextureZipRoot(val prefix: String, val containsTexture2dDir: Boolean) + + private fun textureDir(context: Context): File { + return File(context.filesDir, TEXTURE_DIR) + } + + fun getCachedZipFile(context: Context): File { + return File(context.filesDir, "$CACHE_DIR/$CACHE_ZIP_NAME") + } + + fun getLocalVersion(context: Context): String? { + val versionFile = File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME") + if (versionFile.exists()) { + versionFile.readText().trim().takeIf { it.isNotEmpty() }?.let { return it } + } + return null + } + + fun getInstalledVersion(context: Context): String? { + val versionFile = File(textureDir(context), VERSION_FILE_NAME) + if (!versionFile.exists()) { + return null + } + return versionFile.readText().trim() + } + + private fun ensureCacheDir(context: Context): File { + val basePath = File(context.filesDir, CACHE_DIR) + if (!basePath.exists()) { + basePath.mkdirs() + } + return basePath + } + + private fun saveCachedVersion(context: Context, version: String) { + val versionFile = File(ensureCacheDir(context), CACHE_VERSION_NAME) + versionFile.writeText(version) + } + + private fun findTextureZipRoot(zipFile: ZipFile): TextureZipRoot? { + val entries = zipFile.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (entry.isDirectory) continue + + val name = entry.name.replace('\\', '/').trimStart('/') + val textureVersionMarker = "texture2d/$VERSION_FILE_NAME" + if (name.endsWith(textureVersionMarker)) { + return TextureZipRoot(name.substring(0, name.length - textureVersionMarker.length), true) + } + if (name == VERSION_FILE_NAME || name.endsWith("/$VERSION_FILE_NAME")) { + val prefix = name.substring(0, name.length - VERSION_FILE_NAME.length) + return TextureZipRoot(prefix, false) + } + } + return null + } + + private fun readTextureVersion(zipFile: ZipFile, root: TextureZipRoot): String? { + val versionPath = if (root.containsTexture2dDir) { + "${root.prefix}texture2d/$VERSION_FILE_NAME" + } else { + "${root.prefix}$VERSION_FILE_NAME" + } + val entry = zipFile.getEntry(versionPath) ?: return null + return zipFile.getInputStream(entry).bufferedReader().use { it.readText().trim() } + } + + private fun validateTextureZip(zipFile: File): String { + ZipFile(zipFile).use { zip -> + val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip") + return readTextureVersion(zip, root) ?: throw IOException("texture_version.txt is empty") + } + } + + private fun findTextureZipAsset(releaseData: GithubReleaseModel): Asset? { + val zipAssets = releaseData.assets.filter { it.name.endsWith(".zip", ignoreCase = true) } + return zipAssets.firstOrNull { it.name.equals("texture2d.zip", ignoreCase = true) } + ?: zipAssets.firstOrNull() + } + + private fun safeOutputFile(baseDir: File, relativePath: String): File? { + val targetFile = File(baseDir, relativePath) + val baseCanonical = baseDir.canonicalFile + val targetCanonical = targetFile.canonicalFile + val basePath = baseCanonical.path + File.separator + if (targetCanonical.path != baseCanonical.path && !targetCanonical.path.startsWith(basePath)) { + return null + } + return targetFile + } + + private fun installTextureZip(context: Context, zipFile: File, version: String? = null) { + ZipFile(zipFile).use { zip -> + val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip") + val installedVersion = version ?: readTextureVersion(zip, root) + ?: throw IOException("texture_version.txt is empty") + val targetDir = textureDir(context) + val tempDir = File(context.filesDir, "$TEXTURE_DIR.tmp") + + if (tempDir.exists()) { + tempDir.deleteRecursively() + } + tempDir.mkdirs() + + val entries = zip.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val name = entry.name.replace('\\', '/').trimStart('/') + + val relativePath = if (root.containsTexture2dDir) { + val rootPath = "${root.prefix}texture2d/" + if (!name.startsWith(rootPath)) continue + name.substring(rootPath.length) + } else { + if (!name.startsWith(root.prefix)) continue + name.substring(root.prefix.length) + } + + if (relativePath.isEmpty()) continue + val targetFile = safeOutputFile(tempDir, relativePath) ?: continue + + if (entry.isDirectory) { + targetFile.mkdirs() + continue + } + + targetFile.parentFile?.mkdirs() + zip.getInputStream(entry).use { input -> + FileOutputStream(targetFile).use { output -> + input.copyTo(output) + } + } + } + + File(tempDir, VERSION_FILE_NAME).writeText(installedVersion) + + if (targetDir.exists()) { + targetDir.deleteRecursively() + } + if (!tempDir.renameTo(targetDir)) { + tempDir.copyRecursively(targetDir, overwrite = true) + tempDir.deleteRecursively() + } + } + } + + fun updateTextureFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File, + deleteAfterUpdate: Boolean) { + try { + GakumasHookMain.showToast("Updating texture files from zip...") + + val tempZipFile = File(filesDir, "texture_update.zip") + activity.contentResolver.openInputStream(zipFileUri).use { input -> + if (input == null) { + Log.e(TAG, "texture zip openInputStream failed.") + GakumasHookMain.showToast("Texture update file not found.") + return + } + tempZipFile.outputStream().use { output -> + input.copyTo(output) + } + } + + installTextureZip(activity, tempZipFile) + Log.i(TAG, "Texture zip installed into ${File(filesDir, TEXTURE_DIR).absolutePath}, " + + "version=${getInstalledVersion(activity)}") + tempZipFile.delete() + + if (deleteAfterUpdate) { + activity.contentResolver.delete(zipFileUri, null, null) + } + GakumasHookMain.showToast("Texture update success.") + } + catch (e: java.io.FileNotFoundException) { + Log.i(TAG, "updateTextureFilesFromZip - file not found: $e") + GakumasHookMain.showToast("Texture update file not found.") + } + catch (e: Exception) { + Log.e(TAG, "updateTextureFilesFromZip failed: $e") + GakumasHookMain.showToast("Texture update failed: $e") + } + } + + fun checkUpdateTextureAssets(context: Context, apiURL: String, + onFailed: (Int, String) -> Unit, + onResult: (data: GithubReleaseModel, localVersion: String?) -> Unit) { + runCatching { + val request = Request.Builder() + .url(apiURL) + .build() + FileDownloader.requestGet(request, object : Callback { + override fun onFailure(call: Call, e: IOException) { + onFailed(-1, e.toString()) + } + + override fun onResponse(call: Call, response: Response) { + runCatching { + response.use { + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + val responseBody = response.body?.string() + if (responseBody != null) { + val json = Json { ignoreUnknownKeys = true } + val releaseData = json.decodeFromString(responseBody) + onResult(releaseData, getLocalVersion(context)) + } else { + onFailed(-1, "Response body is null") + } + } + }.onFailure { e -> + Log.e(TAG, "checkUpdateTextureAssets failed", e) + onFailed(-1, e.toString()) + } + } + }) + }.onFailure { e -> + Log.e(TAG, "checkUpdateTextureAssets failed", e) + onFailed(-1, e.toString()) + } + } + + fun updateTextureAssets(context: Context, apiURL: String, + deleteAfterUpdate: Boolean, + onDownload: (Float, downloaded: Long, size: Long) -> Unit, + onFailed: (Int, String) -> Unit, + onSuccess: (version: String, changed: Boolean) -> Unit) { + runCatching { + val request = Request.Builder() + .url(apiURL) + .build() + FileDownloader.requestGet(request, object : Callback { + override fun onFailure(call: Call, e: IOException) { + onFailed(-1, e.toString()) + } + + override fun onResponse(call: Call, response: Response) { + runCatching { + response.use { + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + val responseBody = response.body?.string() + if (responseBody != null) { + val json = Json { ignoreUnknownKeys = true } + val releaseData = json.decodeFromString(responseBody) + val releaseVersion = releaseData.tag_name + val localVersion = getLocalVersion(context) + if (releaseVersion == localVersion) { + onSuccess(releaseVersion, false) + return + } + + val zipAsset = findTextureZipAsset(releaseData) + if (zipAsset == null) { + onFailed(-1, "No zip asset found") + return + } + + val cacheFile = getCachedZipFile(context) + FileDownloader.downloadFileToPath(zipAsset.browser_download_url, + cacheFile, + onDownload, { + runCatching { + saveCachedVersion(context, releaseVersion) + val zipVersion = validateTextureZip(cacheFile) + if (zipVersion != releaseVersion) { + cacheFile.delete() + File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME").delete() + throw IOException("texture_version.txt ($zipVersion) differs from release tag ($releaseVersion)") + } + Log.i(TAG, "Texture zip cached: ${cacheFile.absolutePath}, " + + "size=${cacheFile.length()}, version=$releaseVersion") + onSuccess(releaseVersion, true) + }.onFailure { e -> + Log.e(TAG, "save texture zip failed", e) + onFailed(-1, e.toString()) + } + }, + onFailed) + } else { + onFailed(-1, "Response body is null") + } + } + }.onFailure { e -> + Log.e(TAG, "updateTextureAssets failed", e) + onFailed(-1, e.toString()) + } + } + }) + }.onFailure { e -> + Log.e(TAG, "updateTextureAssets failed", e) + onFailed(-1, e.toString()) + } + } +} diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt index d390abf..51bf0f0 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt @@ -8,9 +8,11 @@ data class GakumasConfig ( var enabled: Boolean = true, var lazyInit: Boolean = true, var replaceFont: Boolean = true, + var replaceTexture: Boolean = true, var textTest: Boolean = false, var useMasterTrans: Boolean = true, var dumpText: Boolean = false, + var dumpRuntimeTexture: Boolean = false, var gameOrientation: Int = 0, var forceExportResource: Boolean = false, var enableFreeCamera: Boolean = false, diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt index 1ac31c7..73178b0 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ProgramConfig.kt @@ -19,6 +19,9 @@ data class ProgramConfig( var useAPIAssets: Boolean = false, var useAPIAssetsURL: String = "", var delRemoteAfterUpdate: Boolean = true, + var useAPITextureAssets: Boolean = false, + var useAPITextureAssetsURL: String = "", + var delTextureRemoteAfterUpdate: Boolean = true, var cleanLocalAssets: Boolean = false, // var localAPIAssetsVersion: String = "", diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt index 253016c..0651a18 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/ViewModels.kt @@ -43,11 +43,12 @@ class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Bool class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig, - private val localResourceVersion: String) : ViewModelProvider.Factory { + private val localResourceVersion: String, + private val localTextureResourceVersion: String) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) { @Suppress("UNCHECKED_CAST") - return ProgramConfigViewModel(initialValue, localResourceVersion) as T + return ProgramConfigViewModel(initialValue, localResourceVersion, localTextureResourceVersion) as T } throw IllegalArgumentException("Unknown ViewModel class") } @@ -62,7 +63,8 @@ data class ConfirmStateModel( var p: Boolean = false ) -class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() { +class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String, + initLocalTextureResourceVersion: String) : ViewModel() { val configState = MutableStateFlow(initValue) val config: StateFlow = configState.asStateFlow() @@ -81,6 +83,18 @@ class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: val errorStringState = MutableStateFlow("") val errorString: StateFlow = errorStringState.asStateFlow() + val textureDownloadProgressState = MutableStateFlow(-1f) + val textureDownloadProgress: StateFlow = textureDownloadProgressState.asStateFlow() + + val textureDownloadAbleState = MutableStateFlow(true) + val textureDownloadAble: StateFlow = textureDownloadAbleState.asStateFlow() + + val localTextureResourceVersionState = MutableStateFlow(initLocalTextureResourceVersion) + val localTextureResourceVersion: StateFlow = localTextureResourceVersionState.asStateFlow() + + val textureErrorStringState = MutableStateFlow("") + val textureErrorString: StateFlow = textureErrorStringState.asStateFlow() + val mainUIConfirmState = MutableStateFlow(ConfirmStateModel()) val mainUIConfirm: StateFlow = mainUIConfirmState.asStateFlow() } diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt index 660597a..8cd8ddb 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/MainPage.kt @@ -49,14 +49,14 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null, previewData: GakumasConfig? = null) { val imagePainter = painterResource(R.drawable.bg_pattern) var versionInfo by remember { - mutableStateOf(context?.getVersion() ?: listOf("", "Unknown")) + mutableStateOf(context?.getVersion() ?: listOf("", "Unknown", "Unknown")) } // val config = getConfigState(context, previewData) val confirmState by getMainUIConfirmState(context, null) val programConfig by getProgramConfigState(context) LaunchedEffect(programConfig) { - versionInfo = context?.getVersion() ?: listOf("", "Unknown") + versionInfo = context?.getVersion() ?: listOf("", "Unknown", "Unknown") } Box( @@ -79,6 +79,7 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null, ) { Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp) Text(text = "Assets version: ${versionInfo[1]}", fontSize = 13.sp) + Text(text = "Texture version: ${versionInfo[2]}", fontSize = 13.sp) SettingsTabs(modifier, listOf(stringResource(R.string.about), stringResource(R.string.home), stringResource(R.string.advanced_settings)), diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt index 70145dd..e37fd32 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/AdvancedSettingsPage.kt @@ -87,6 +87,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier, v -> context?.onDumpTextChanged(v) } + GakuSwitch(modifier, stringResource(R.string.dump_runtime_texture), checked = config.value.dumpRuntimeTexture) { + v -> context?.onDumpRuntimeTextureChanged(v) + } + GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) { v -> context?.onForceExportResourceChanged(v) } diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt index 988fe47..d99b573 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/ui/pages/subPages/HomePage.kt @@ -43,11 +43,16 @@ import io.github.chinosk.gakumas.localify.getProgramConfigState import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState import io.github.chinosk.gakumas.localify.getProgramDownloadState +import io.github.chinosk.gakumas.localify.getProgramLocalTextureResourceVersionState import io.github.chinosk.gakumas.localify.getProgramLocalResourceVersionState import io.github.chinosk.gakumas.localify.getProgramLocalAPIResourceVersionState +import io.github.chinosk.gakumas.localify.getProgramTextureDownloadAbleState +import io.github.chinosk.gakumas.localify.getProgramTextureDownloadErrorStringState +import io.github.chinosk.gakumas.localify.getProgramTextureDownloadState import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker +import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater import io.github.chinosk.gakumas.localify.mainUtils.TimeUtils import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel @@ -75,6 +80,10 @@ fun HomePage(modifier: Modifier = Modifier, val localResourceVersion by getProgramLocalResourceVersionState(context) val localAPIResourceVersion by getProgramLocalAPIResourceVersionState(context) val downloadErrorString by getProgramDownloadErrorStringState(context) + val textureDownloadProgress by getProgramTextureDownloadState(context) + val textureDownloadAble by getProgramTextureDownloadAbleState(context) + val localTextureResourceVersion by getProgramLocalTextureResourceVersionState(context) + val textureDownloadErrorString by getProgramTextureDownloadErrorStringState(context) var isFirstTimeInThisPage by rememberSaveable { mutableStateOf(true) } // val scrollState = rememberScrollState() @@ -131,7 +140,8 @@ fun HomePage(modifier: Modifier = Modifier, }) } - fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true) { + fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true, + onFinished: (() -> Unit)? = null) { context?.mainPageAssetsViewDataUpdate( downloadAbleState = false, errorString = "", @@ -139,6 +149,7 @@ fun HomePage(modifier: Modifier = Modifier, ) if (isZipResource) { zipResourceDownload() + onFinished?.invoke() } else { RemoteAPIFilesChecker.checkUpdateLocalAssets(context!!, @@ -150,6 +161,7 @@ fun HomePage(modifier: Modifier = Modifier, downloadProgressState = -1f ) context.mainUIConfirmStatUpdate(true, "Error", reason) + onFinished?.invoke() }, onResult = { data, localVersion -> if (!isHumanClick) { @@ -159,6 +171,7 @@ fun HomePage(modifier: Modifier = Modifier, errorString = "", downloadProgressState = -1f ) + onFinished?.invoke() return@checkUpdateLocalAssets } } @@ -170,10 +183,13 @@ fun HomePage(modifier: Modifier = Modifier, onDownload = { progress, _, _ -> context.mainPageAssetsViewDataUpdate(downloadProgressState = progress) }, - onFailed = { _, reason -> context.mainPageAssetsViewDataUpdate( - downloadAbleState = true, - errorString = reason, - )}, + onFailed = { _, reason -> + context.mainPageAssetsViewDataUpdate( + downloadAbleState = true, + errorString = reason, + ) + onFinished?.invoke() + }, onSuccess = { saveFile, releaseVersion -> context.mainPageAssetsViewDataUpdate( downloadAbleState = true, @@ -185,6 +201,7 @@ fun HomePage(modifier: Modifier = Modifier, ) context.saveProgramConfig() Log.d(TAG, "saved: $releaseVersion $saveFile") + onFinished?.invoke() }) }, onCancel = { @@ -193,12 +210,92 @@ fun HomePage(modifier: Modifier = Modifier, errorString = "", downloadProgressState = -1f ) + onFinished?.invoke() } ) }) } } + fun startTextureResourceUpdate() { + context?.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = false, + errorString = "", + downloadProgressState = -1f + ) + TextureResourceUpdater.updateTextureAssets(context!!, + programConfig.value.useAPITextureAssetsURL, + programConfig.value.delTextureRemoteAfterUpdate, + onDownload = { progress, _, _ -> + context.mainPageTextureAssetsViewDataUpdate(downloadProgressState = progress) + }, + onFailed = { _, reason -> + context.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = true, + errorString = reason, + ) + }, + onSuccess = { releaseVersion, changed -> + context.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = true, + errorString = "", + downloadProgressState = -1f, + localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context) + ?: releaseVersion + ) + context.saveProgramConfig() + Log.d(TAG, "texture resource update finished: $releaseVersion changed=$changed") + }) + } + + fun onClickTextureDownload(isHumanClick: Boolean = true) { + context?.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = false, + errorString = "", + downloadProgressState = -1f + ) + TextureResourceUpdater.checkUpdateTextureAssets(context!!, + programConfig.value.useAPITextureAssetsURL, + onFailed = { _, reason -> + context.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = true, + errorString = reason, + downloadProgressState = -1f + ) + if (isHumanClick) { + context.mainUIConfirmStatUpdate(true, "Error", reason) + } + }, + onResult = { data, localVersion -> + if (!isHumanClick) { + if (data.tag_name == localVersion) { + context.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = true, + errorString = "", + downloadProgressState = -1f, + localTextureResourceVersion = localVersion + ) + return@checkUpdateTextureAssets + } + } + + context.mainUIConfirmStatUpdate(true, context.getString(R.string.texture_resource_update), + "${data.name}\n$localVersion -> ${data.tag_name}\n${data.body}\n\n${TimeUtils.convertIsoToLocalTime(data.published_at)}", + onConfirm = { + resourceSettingsViewModel.expanded = true + startTextureResourceUpdate() + }, + onCancel = { + context.mainPageTextureAssetsViewDataUpdate( + downloadAbleState = true, + errorString = "", + downloadProgressState = -1f + ) + } + ) + }) + } + LaunchedEffect(Unit) { try { if (context == null) return@LaunchedEffect @@ -206,9 +303,25 @@ fun HomePage(modifier: Modifier = Modifier, context.mainPageAssetsViewDataUpdate( localAPIResourceVersion = localAPIResVer ) + context.mainPageTextureAssetsViewDataUpdate( + localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context) + ) if (isFirstTimeInThisPage) { - if (programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()) { - onClickDownload(false, false) + val shouldCheckResource = + programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty() + val shouldCheckTexture = config.value.replaceTexture && + programConfig.value.useAPITextureAssets && + programConfig.value.useAPITextureAssetsURL.isNotEmpty() + + if (shouldCheckResource) { + onClickDownload(false, false) { + if (shouldCheckTexture) { + onClickTextureDownload(false) + } + } + } + else if (shouldCheckTexture) { + onClickTextureDownload(false) } } } @@ -240,6 +353,10 @@ fun HomePage(modifier: Modifier = Modifier, v -> context?.onReplaceFontChanged(v) } + GakuSwitch(modifier, stringResource(R.string.replace_texture), checked = config.value.replaceTexture) { + v -> context?.onReplaceTextureChanged(v) + } + } } Spacer(Modifier.height(6.dp)) @@ -467,6 +584,104 @@ fun HomePage(modifier: Modifier = Modifier, } } + + if (config.value.replaceTexture) { + item { + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + ) + } + + item { + GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp), + checked = programConfig.value.useAPITextureAssets, + text = stringResource(R.string.check_texture_resource_from_api) + ) { v -> context?.onPUseAPITextureAssetsChanged(v) } + + CollapsibleBox(modifier = modifier.graphicsLayer(clip = false), + expandState = programConfig.value.useAPITextureAssets, + collapsedHeight = 0.dp, + innerPaddingLeftRight = 8.dp, + showExpand = false + ) { + GakuSwitch(modifier = modifier, + checked = programConfig.value.delTextureRemoteAfterUpdate, + text = stringResource(id = R.string.del_remote_after_update) + ) { v -> context?.onPDelTextureRemoteAfterUpdateChanged(v) } + + LazyColumn(modifier = modifier + .sizeIn(maxHeight = screenH), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + Row(modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically) { + + GakuTextInput(modifier = modifier + .height(45.dp) + .padding(end = 8.dp) + .fillMaxWidth() + .weight(1f), + fontSize = 14f, + value = programConfig.value.useAPITextureAssetsURL, + onValueChange = { c -> context?.onPUseAPITextureAssetsURLChanged(c, 0, 0, 0)}, + label = { Text(stringResource(R.string.texture_api_addr)) } + ) + + if (textureDownloadAble) { + GakuButton(modifier = modifier + .height(40.dp) + .sizeIn(minWidth = 80.dp), + text = stringResource(R.string.check_update), + onClick = { onClickTextureDownload(true) }) + } + else { + GakuButton(modifier = modifier + .height(40.dp) + .sizeIn(minWidth = 80.dp), + text = stringResource(id = R.string.cancel), onClick = { + FileDownloader.cancel() + }) + } + + } + } + + if (textureDownloadProgress >= 0) { + item { + GakuProgressBar(progress = textureDownloadProgress, + isError = textureDownloadErrorString.isNotEmpty()) + } + } + + if (textureDownloadErrorString.isNotEmpty()) { + item { + Text(text = textureDownloadErrorString, color = Color(0xFFE2041B)) + } + } + + item { + Text(modifier = Modifier + .fillMaxWidth() + .clickable { + context?.let { + it.mainPageTextureAssetsViewDataUpdate( + localTextureResourceVersion = TextureResourceUpdater + .getLocalVersion(it) + ) + } + }, text = "${stringResource(R.string.downloaded_texture_resource_version)}: $localTextureResourceVersion") + } + + item { + Spacer(Modifier.height(0.dp)) + } + } + } + } + } } } } diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1f3a5e0..16a3686 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -91,6 +91,7 @@ エラー: 無効 エラー テキストをエクスポート + 実行時テクスチャをダンプ ドロップダウンメニューを表示 com.google.android.material.transformation.FabTransformationScrimBehavior com.google.android.material.transformation.FabTransformationSheetBehavior @@ -311,6 +312,7 @@ Xposed スコープは再パッチなしで動的に変更が可能です。 範囲の開始 RenderScale (0.5/0.59/0.67/0.77/1.0) フォントを置換する + テクスチャを置換する パッチ済みの APK を予約する リソース設定 リソース URL @@ -352,4 +354,9 @@ Xposed スコープは再パッチなしで動的に変更が可能です。 胸の大きさを使用する 最高 警告 + API からテクスチャリソースの更新を確認 + テクスチャ API アドレス (GitHub 最新リリース API) + テクスチャリソースを更新 + ダウンロード済みテクスチャバージョン + http://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f83377e..435931e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3,6 +3,7 @@ Gakumas Localify 启用插件 (不可热重载) 替换字体 + 替换贴图 快速初始化(懒加载配置) 启用自由视角(可热重载; 需使用实体键盘) 以上述配置启动游戏/重载配置 @@ -17,6 +18,7 @@ 文本 hook 测试模式 使用 MasterDB 本地化 导出文本 + 导出运行时贴图 启动后强制导出资源 以 iOS 登陆 极高 @@ -86,6 +88,10 @@ API 地址(Github Latest Release API) 检查更新 翻译资源更新 + 从服务器检查贴图资源更新 + 贴图 API 地址(Github Latest Release API) + 贴图资源更新 + 已下载贴图资源版本 游戏修补 修补模式 @@ -105,4 +111,5 @@ about_contributors_zh_cn.json https://uma.chinosk6.cn/api/gkms_trans_data - \ No newline at end of file + http://texture.gakumas.cn/api/gkms_texture_data + diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000..78336b6 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,113 @@ + + Gakumas Localify + Gakumas Localify + 啟用插件 (不可熱重載) + 替換字體 + 快速初始化(懶人設定) + 啟用自由視角(可熱重載; 需使用實體鍵盤) + 以上述設定啟動遊戲/重載設定 + 最大 FPS (0 為保持遊戲原設定) + 解鎖所有 Live + 解鎖所有 Live 服裝 + Live 使用自定義角色 + Live 自定義頭部 ID (例: costume_head_hski-cstm-0002) + Live 自定義服裝 ID (例: hski-cstm-0002) + 使用自定義畫質設定 + RenderScale (0.5/0.59/0.67/0.77/1.0) + 文本 hook 測試模式 + 使用 MasterDB 翻譯 + 導出文本 + 導出運行時貼圖 + 啟動後強制導出資源 + 模擬以 iOS 登入 + 極高 + 超高 + + + + 原版 + 豎屏 + 橫屏 + 方向鎖定 + 啟用胸部參數 + 阻尼 (Damping) + 剛度 (Stiffness) + 彈簧係數 (Spring) + 鐘擺係數 (Pendulum) + 鐘擺範圍 (PendulumRange) + Average + RootWeight + 範圍限制倍率 (0 為不限制, 1 為原版) + 使用手臂矯正 + IsDirty + 應用縮放 + 胸部縮放倍率 + 啟用範圍限制倍率 + axisX.x + axisY.x + axisZ.x + axisX.y + axisY.y + axisZ.y + 基本設定 + 畫面設定 + 攝影機設定 + 測試模式 - LIVE + 調試設定 + 胸部參數 + 關於 + 主頁 + 進階設定 + 使用前警告 + 本插件僅供學習和交流使用。 + 使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。 + 關於本插件 + 本插件完全免費。若您付費購買了本插件,請檢舉店家。 + 插件交流QQ群: 991990192 + 項目貢獻 + 插件本體 + 貢獻者列表 + 譯文倉庫 + 資源設定 + 檢查內置翻譯資源更新 + 清除遊戲目錄內的插件翻譯資源 + 使用雲端 ZIP 翻譯資源 + 資源地址 + 下載 + 文件解析失敗 + 此 ZIP 文件不是一個有效的翻譯資源包 + 取消 + 確定 + 已下載資源版本 + 替換文件後刪除下載緩存 + 注意 + 安裝 + 安裝中 + 从伺服器檢查更新資源 + API 地址(Github Latest Release API) + 檢查更新 + 翻譯資源更新 + 从伺服器檢查貼圖資源更新 + 貼圖 API 地址(Github Latest Release API) + 貼圖資源更新 + 已下載貼圖資源版本 + 遊戲修補 + 修補模式 + 本地模式 + 為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。 + 集成模式 + 修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。 + Shizuku 服務可用 + Shizuku 服務未連接 + 部分功能不可用 + 可調試 + 安裝時保留修補包 + 支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip + 由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。 + 您確定要刪除吗 + 修補完成,是否開始安裝? + + about_contributors_zh_cn.json + https://uma.chinosk6.cn/api/gkms_trans_data + http://texture.gakumas.cn/api/gkms_texture_data + diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml new file mode 100644 index 0000000..78336b6 --- /dev/null +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -0,0 +1,113 @@ + + Gakumas Localify + Gakumas Localify + 啟用插件 (不可熱重載) + 替換字體 + 快速初始化(懶人設定) + 啟用自由視角(可熱重載; 需使用實體鍵盤) + 以上述設定啟動遊戲/重載設定 + 最大 FPS (0 為保持遊戲原設定) + 解鎖所有 Live + 解鎖所有 Live 服裝 + Live 使用自定義角色 + Live 自定義頭部 ID (例: costume_head_hski-cstm-0002) + Live 自定義服裝 ID (例: hski-cstm-0002) + 使用自定義畫質設定 + RenderScale (0.5/0.59/0.67/0.77/1.0) + 文本 hook 測試模式 + 使用 MasterDB 翻譯 + 導出文本 + 導出運行時貼圖 + 啟動後強制導出資源 + 模擬以 iOS 登入 + 極高 + 超高 + + + + 原版 + 豎屏 + 橫屏 + 方向鎖定 + 啟用胸部參數 + 阻尼 (Damping) + 剛度 (Stiffness) + 彈簧係數 (Spring) + 鐘擺係數 (Pendulum) + 鐘擺範圍 (PendulumRange) + Average + RootWeight + 範圍限制倍率 (0 為不限制, 1 為原版) + 使用手臂矯正 + IsDirty + 應用縮放 + 胸部縮放倍率 + 啟用範圍限制倍率 + axisX.x + axisY.x + axisZ.x + axisX.y + axisY.y + axisZ.y + 基本設定 + 畫面設定 + 攝影機設定 + 測試模式 - LIVE + 調試設定 + 胸部參數 + 關於 + 主頁 + 進階設定 + 使用前警告 + 本插件僅供學習和交流使用。 + 使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。 + 關於本插件 + 本插件完全免費。若您付費購買了本插件,請檢舉店家。 + 插件交流QQ群: 991990192 + 項目貢獻 + 插件本體 + 貢獻者列表 + 譯文倉庫 + 資源設定 + 檢查內置翻譯資源更新 + 清除遊戲目錄內的插件翻譯資源 + 使用雲端 ZIP 翻譯資源 + 資源地址 + 下載 + 文件解析失敗 + 此 ZIP 文件不是一個有效的翻譯資源包 + 取消 + 確定 + 已下載資源版本 + 替換文件後刪除下載緩存 + 注意 + 安裝 + 安裝中 + 从伺服器檢查更新資源 + API 地址(Github Latest Release API) + 檢查更新 + 翻譯資源更新 + 从伺服器檢查貼圖資源更新 + 貼圖 API 地址(Github Latest Release API) + 貼圖資源更新 + 已下載貼圖資源版本 + 遊戲修補 + 修補模式 + 本地模式 + 為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。 + 集成模式 + 修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。 + Shizuku 服務可用 + Shizuku 服務未連接 + 部分功能不可用 + 可調試 + 安裝時保留修補包 + 支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip + 由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。 + 您確定要刪除吗 + 修補完成,是否開始安裝? + + about_contributors_zh_cn.json + https://uma.chinosk6.cn/api/gkms_trans_data + http://texture.gakumas.cn/api/gkms_texture_data + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..78336b6 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,113 @@ + + Gakumas Localify + Gakumas Localify + 啟用插件 (不可熱重載) + 替換字體 + 快速初始化(懶人設定) + 啟用自由視角(可熱重載; 需使用實體鍵盤) + 以上述設定啟動遊戲/重載設定 + 最大 FPS (0 為保持遊戲原設定) + 解鎖所有 Live + 解鎖所有 Live 服裝 + Live 使用自定義角色 + Live 自定義頭部 ID (例: costume_head_hski-cstm-0002) + Live 自定義服裝 ID (例: hski-cstm-0002) + 使用自定義畫質設定 + RenderScale (0.5/0.59/0.67/0.77/1.0) + 文本 hook 測試模式 + 使用 MasterDB 翻譯 + 導出文本 + 導出運行時貼圖 + 啟動後強制導出資源 + 模擬以 iOS 登入 + 極高 + 超高 + + + + 原版 + 豎屏 + 橫屏 + 方向鎖定 + 啟用胸部參數 + 阻尼 (Damping) + 剛度 (Stiffness) + 彈簧係數 (Spring) + 鐘擺係數 (Pendulum) + 鐘擺範圍 (PendulumRange) + Average + RootWeight + 範圍限制倍率 (0 為不限制, 1 為原版) + 使用手臂矯正 + IsDirty + 應用縮放 + 胸部縮放倍率 + 啟用範圍限制倍率 + axisX.x + axisY.x + axisZ.x + axisX.y + axisY.y + axisZ.y + 基本設定 + 畫面設定 + 攝影機設定 + 測試模式 - LIVE + 調試設定 + 胸部參數 + 關於 + 主頁 + 進階設定 + 使用前警告 + 本插件僅供學習和交流使用。 + 使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。 + 關於本插件 + 本插件完全免費。若您付費購買了本插件,請檢舉店家。 + 插件交流QQ群: 991990192 + 項目貢獻 + 插件本體 + 貢獻者列表 + 譯文倉庫 + 資源設定 + 檢查內置翻譯資源更新 + 清除遊戲目錄內的插件翻譯資源 + 使用雲端 ZIP 翻譯資源 + 資源地址 + 下載 + 文件解析失敗 + 此 ZIP 文件不是一個有效的翻譯資源包 + 取消 + 確定 + 已下載資源版本 + 替換文件後刪除下載緩存 + 注意 + 安裝 + 安裝中 + 从伺服器檢查更新資源 + API 地址(Github Latest Release API) + 檢查更新 + 翻譯資源更新 + 从伺服器檢查貼圖資源更新 + 貼圖 API 地址(Github Latest Release API) + 貼圖資源更新 + 已下載貼圖資源版本 + 遊戲修補 + 修補模式 + 本地模式 + 為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。 + 集成模式 + 修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。 + Shizuku 服務可用 + Shizuku 服務未連接 + 部分功能不可用 + 可調試 + 安裝時保留修補包 + 支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip + 由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。 + 您確定要刪除吗 + 修補完成,是否開始安裝? + + about_contributors_zh_cn.json + https://uma.chinosk6.cn/api/gkms_trans_data + http://texture.gakumas.cn/api/gkms_texture_data + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b5b5e6..f359e00 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Gakumas Localify Enable Plugin (Not Hot Reloadable) Replace Font + Replace Texture Fast Initialization (Lazy loading) Enable Free Camera Start Game / Hot Reload Config @@ -17,6 +18,7 @@ Text Hook Test Mode Enable MasterDB Localization Export Text + Dump Runtime Texture Force Update Resource Login as iOS Ultra @@ -86,6 +88,10 @@ API Address(Github Latest Release API) Check Translation Resource Update + Check Texture Resource Update From API + Texture API Address (Github Latest Release API) + Texture Resource Update + Downloaded Texture Version Game Patch Patch Mode @@ -105,4 +111,5 @@ about_contributors_en.json https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest - \ No newline at end of file + http://texture.gakumas.cn/api/gkms_texture_data + From 0200f20c73b901c8a285ab8f5d7c2992a0445bae Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sat, 9 May 2026 20:52:39 +0800 Subject: [PATCH 2/5] =?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 --- app/build.gradle | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 75145c2..2e925de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,18 +6,8 @@ plugins { id("kotlin-parcelize") } -def sharedVersionFile = file("$projectDir/src/main/cpp/GakumasLocalify/VERSION") -if (!sharedVersionFile.exists()) { - throw new GradleException("Shared version file not found: ${sharedVersionFile}") -} -def sharedVersionName = sharedVersionFile.text.trim() -def sharedVersionMatcher = sharedVersionName =~ /^v?(\d+)\.(\d+)\.(\d+).*$/ -if (!sharedVersionMatcher.matches()) { - throw new GradleException("Invalid shared version: ${sharedVersionName}") -} -def sharedVersionCode = sharedVersionMatcher[0][1].toInteger() * 10000 + - sharedVersionMatcher[0][2].toInteger() * 100 + - sharedVersionMatcher[0][3].toInteger() +def pluginVersionName = "v3.3.1" +def pluginVersionCode = 30301 android { namespace 'io.github.chinosk.gakumas.localify' @@ -28,9 +18,9 @@ android { applicationId "io.github.chinosk.gakumas.localify" minSdk 29 targetSdk 34 - versionCode sharedVersionCode - versionName sharedVersionName - buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" + versionCode pluginVersionCode + versionName pluginVersionName + buildConfigField "String", "VERSION_NAME", "\"${pluginVersionName}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From afcf1a3a7c1ba3b774952392182eb2fac1dbd505 Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sat, 9 May 2026 20:55:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e925de..997b241 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,9 +6,6 @@ plugins { id("kotlin-parcelize") } -def pluginVersionName = "v3.3.1" -def pluginVersionCode = 30301 - android { namespace 'io.github.chinosk.gakumas.localify' compileSdk 34 @@ -18,9 +15,9 @@ android { applicationId "io.github.chinosk.gakumas.localify" minSdk 29 targetSdk 34 - versionCode pluginVersionCode - versionName pluginVersionName - buildConfigField "String", "VERSION_NAME", "\"${pluginVersionName}\"" + versionCode 12 + versionName "v3.3.1" + buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -135,4 +132,4 @@ dependencies { implementation(libs.shadowhook) compileOnly(libs.xposed.api) implementation(libs.kotlinx.serialization.json) -} +} \ No newline at end of file From 562b5850b5e5b96dfd7a171aa1ccd16ef6ee19fc Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sat, 9 May 2026 20:57:16 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=B4=B4=E5=9B=BE?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=9B=BF=E6=8D=A2=E7=BD=91=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rHK/strings.xml | 2 +- app/src/main/res/values-zh-rMO/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 16a3686..b024cea 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -358,5 +358,5 @@ Xposed スコープは再パッチなしで動的に変更が可能です。 テクスチャ API アドレス (GitHub 最新リリース API) テクスチャリソースを更新 ダウンロード済みテクスチャバージョン - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 435931e..a56e522 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -111,5 +111,5 @@ about_contributors_zh_cn.json https://uma.chinosk6.cn/api/gkms_trans_data - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 78336b6..09d21dd 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -109,5 +109,5 @@ about_contributors_zh_cn.json https://uma.chinosk6.cn/api/gkms_trans_data - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 78336b6..09d21dd 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -109,5 +109,5 @@ about_contributors_zh_cn.json https://uma.chinosk6.cn/api/gkms_trans_data - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 78336b6..09d21dd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -109,5 +109,5 @@ about_contributors_zh_cn.json https://uma.chinosk6.cn/api/gkms_trans_data - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f359e00..09dcf90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,5 +111,5 @@ about_contributors_en.json https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest - http://texture.gakumas.cn/api/gkms_texture_data + https://texture.gakumas.cn/api/gkms_texture_data From b4955e94eec31fe51d790589689502287bce2aba Mon Sep 17 00:00:00 2001 From: pm chihya Date: Sun, 10 May 2026 16:25:38 +0800 Subject: [PATCH 5/5] =?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 --- app/src/main/cpp/CMakeLists.txt | 1 + app/src/main/cpp/GakumasLocalify/Hook.cpp | 776 +---------------- .../main/cpp/GakumasLocalify/HookTexture.cpp | 797 ++++++++++++++++++ .../main/cpp/GakumasLocalify/HookTexture.h | 23 + 4 files changed, 822 insertions(+), 775 deletions(-) create mode 100644 app/src/main/cpp/GakumasLocalify/HookTexture.cpp create mode 100644 app/src/main/cpp/GakumasLocalify/HookTexture.h diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index d948cd3..bf9db20 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED libMarryKotone.cpp GakumasLocalify/Plugin.cpp GakumasLocalify/Hook.cpp + GakumasLocalify/HookTexture.cpp GakumasLocalify/Log.cpp GakumasLocalify/Misc.cpp GakumasLocalify/Local.cpp diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp index b197d67..a6b64d8 100644 --- a/app/src/main/cpp/GakumasLocalify/Hook.cpp +++ b/app/src/main/cpp/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/app/src/main/cpp/GakumasLocalify/HookTexture.cpp b/app/src/main/cpp/GakumasLocalify/HookTexture.cpp new file mode 100644 index 0000000..5d416dc --- /dev/null +++ b/app/src/main/cpp/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/app/src/main/cpp/GakumasLocalify/HookTexture.h b/app/src/main/cpp/GakumasLocalify/HookTexture.h new file mode 100644 index 0000000..e5b3ded --- /dev/null +++ b/app/src/main/cpp/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