From 35c2b9f4899090749adddc594d730970a7404976 Mon Sep 17 00:00:00 2001 From: chinosk <2248589280@qq.com> Date: Sun, 5 Jan 2025 22:36:12 +0000 Subject: [PATCH] MasterDB Localization --- app/build.gradle | 4 +- app/src/main/cpp/CMakeLists.txt | 1 + app/src/main/cpp/GakumasLocalify/Hook.cpp | 161 +++- .../main/cpp/GakumasLocalify/Il2cppUtils.hpp | 124 ++- app/src/main/cpp/GakumasLocalify/Local.h | 3 + .../main/cpp/GakumasLocalify/MasterLocal.cpp | 807 ++++++++++++++++++ .../main/cpp/GakumasLocalify/MasterLocal.h | 12 + .../GakumasLocalify/MasterLocal_Legacy.cpp | 786 +++++++++++++++++ .../cpp/GakumasLocalify/MasterLocal_Legacy.h | 15 + app/src/main/cpp/GakumasLocalify/Misc.cpp | 27 + app/src/main/cpp/GakumasLocalify/Misc.hpp | 2 + .../cpp/GakumasLocalify/config/Config.cpp | 2 + .../cpp/GakumasLocalify/config/Config.hpp | 1 + .../cpp/deps/UnityResolve/UnityResolve.hpp | 2 +- .../gakumas/localify/ConfigUpdateListener.kt | 6 + .../localify/hookUtils/FilesChecker.kt | 4 + .../gakumas/localify/models/GakumasConfig.kt | 1 + .../ui/pages/subPages/AdvancedSettingsPage.kt | 4 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 +- 22 files changed, 1927 insertions(+), 40 deletions(-) create mode 100644 app/src/main/cpp/GakumasLocalify/MasterLocal.cpp create mode 100644 app/src/main/cpp/GakumasLocalify/MasterLocal.h create mode 100644 app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.cpp create mode 100644 app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.h diff --git a/app/build.gradle b/app/build.gradle index 4ccde72..efddf18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "io.github.chinosk.gakumas.localify" minSdk 29 targetSdk 34 - versionCode 5 - versionName "v1.6.8" + versionCode 10 + versionName "v2.0.1 - Beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 8f3f600..d948cd3 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -42,6 +42,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED GakumasLocalify/Log.cpp GakumasLocalify/Misc.cpp GakumasLocalify/Local.cpp + GakumasLocalify/MasterLocal.cpp GakumasLocalify/camera/baseCamera.cpp GakumasLocalify/camera/camera.cpp GakumasLocalify/config/Config.cpp diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp index b57cad2..5802afb 100644 --- a/app/src/main/cpp/GakumasLocalify/Hook.cpp +++ b/app/src/main/cpp/GakumasLocalify/Hook.cpp @@ -5,6 +5,7 @@ #include "../deps/UnityResolve/UnityResolve.hpp" #include "Il2cppUtils.hpp" #include "Local.h" +#include "MasterLocal.h" #include #include "camera/camera.hpp" #include "config/Config.hpp" @@ -12,6 +13,7 @@ #include #include #include +#include std::unordered_set hookedStubs{}; @@ -421,6 +423,66 @@ namespace GakumasLocal::HookMain { TextField_set_value_Orig(self, value); } + // 未使用的 Hook + DEFINE_HOOK(void, EffectGroup_ctor, (void* self, void* mtd)) { + // auto self_klass = Il2cppUtils::get_class_from_instance(self); + // Log::DebugFmt("EffectGroup_ctor: self: %s::%s", self_klass->namespaze, self_klass->name); + EffectGroup_ctor_Orig(self, mtd); + } + + // 用于本地化 MasterDB + DEFINE_HOOK(void, MessageExtensions_MergeFrom, (void* message, void* span, void* mtd)) { + MessageExtensions_MergeFrom_Orig(message, span, mtd); + if (message) { + auto ret_klass = Il2cppUtils::get_class_from_instance(message); + if (ret_klass) { + // Log::DebugFmt("LocalizeMasterItem: %s", ret_klass->name); + MasterLocal::LocalizeMasterItem(message, ret_klass->name); + } + } + } + + /* + // 未使用的 Hook + DEFINE_HOOK(void, MasterBase_GetAll, (void* self, UnityResolve::UnityType::Array* getAllSQL, + int sqlLength, UnityResolve::UnityType::List* result, void* predicate, void* comparison, void* mtd)) { + // result: List, 和 query 的表名一致 + + MasterBase_GetAll_Orig(self, getAllSQL, sqlLength, result, predicate, comparison, mtd); + + auto data_ptr = reinterpret_cast(getAllSQL->GetData()); + std::string qS(data_ptr, data_ptr + sqlLength); + + + Il2cppUtils::Tools::CSListEditor resultList(result); + MasterLocal::LocalizeMaster(qS, result); + } + + void LocalizeFindByKey(void* result, void* self) { + return; // 暂时不需要了 + auto self_klass = Il2cppUtils::get_class_from_instance(self); + Log::DebugFmt("Localize: %s", self_klass->name); // FeatureLockMaster + // return; + + if (!result) return; + auto result_klass = Il2cppUtils::get_class_from_instance(result); + std::string klassName = result_klass->name; + + auto MasterBase_klass = Il2cppUtils::get_class_from_instance(self); + auto MasterBase_GetTableName = Il2cppUtils::il2cpp_class_get_method_from_name(MasterBase_klass, "GetTableName", 0); + if (MasterBase_GetTableName) { + auto tableName = reinterpret_cast(MasterBase_GetTableName->methodPointer)(self, MasterBase_GetTableName); + // Log::DebugFmt("MasterBase_FindByKey: %s", tableName->ToString().c_str()); + + if (klassName == "List`1") { + MasterLocal::LocalizeMaster(result, tableName->ToString()); + } + else { + MasterLocal::LocalizeMasterItem(result, tableName->ToString()); + } + } + }*/ + DEFINE_HOOK(Il2cppString*, OctoCaching_GetResourceFileName, (void* data, void* method)) { auto ret = OctoCaching_GetResourceFileName_Orig(data, method); //Log::DebugFmt("OctoCaching_GetResourceFileName: %s", ret->ToString().c_str()); @@ -1093,6 +1155,68 @@ namespace GakumasLocal::HookMain { return CampusActorAnimation_Setup_Orig(self, rootTrans, initializeData); } +/* + std::map> findByKeyHookAddress{}; + void* FindByKeyHooks(void* self, void* key, void* mtd) { + auto self_klass = Il2cppUtils::get_class_from_instance(self); + + if (auto it = findByKeyHookAddress.find(self_klass->name); it != findByKeyHookAddress.end()) { + Log::DebugFmt("FindByKeyHooks Call cache: %s, %p, %p", self_klass->name, it->second.first, it->second.second); + return reinterpret_cast(it->second.second)(self, key, mtd); + } + Log::DebugFmt("FindByKeyHooks not in cache: %s", self_klass->name); + + auto FindByKey_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(self_klass, "FindByKey", 1); + for (auto& [k, v] : findByKeyHookAddress) { + if (FindByKey_mtd->methodPointer == v.first) { + findByKeyHookAddress.emplace(self_klass->name, std::make_pair(FindByKey_mtd->methodPointer, v.second)); + Log::DebugFmt("FindByKeyHooks add to cache: %s", self_klass->name); + return reinterpret_cast(v.second)(self, key, mtd); + } + } + + Log::ErrorFmt("FindByKeyHooks not found hook: %s", self_klass->name); + return SHADOWHOOK_CALL_PREV(FindByKeyHooks, self, key, mtd); + } + + static inline std::vector g_registerMasterFindByKeyHookFuncs; + +#define DEF_AND_ADD_MASTER_FINDBYKEY_HOOK(name) \ + using name##_FindByKey_Type = void* (*)(void* self, void* key, void* idx, void* mtd); \ + inline name##_FindByKey_Type name##_FindByKey_Addr = nullptr; \ + inline void* name##_FindByKey_Orig = nullptr; \ + inline void* name##_FindByKey_Hook(void* self, void* key, void* idx, void* mtd) { \ + auto result = reinterpret_cast( \ + name##_FindByKey_Orig)(self, key, idx, mtd); \ + LocalizeFindByKey(result, self); \ + return result; \ + } \ + inline void name##_RegisterHook(HookInstaller* hookInstaller) { \ + auto klass = Il2cppUtils::GetClass( \ + "Assembly-CSharp.dll", "Campus.Common.Master", #name); \ + auto mtd = Il2cppUtils::il2cpp_class_get_method_from_name( \ + klass->address, "GetData", 2); \ + ADD_HOOK(name##_FindByKey, mtd->methodPointer); \ + } \ + struct name##_RegisterHookPusher { \ + name##_RegisterHookPusher() { \ + g_registerMasterFindByKeyHookFuncs.push_back(&name##_RegisterHook);\ + } \ + } g_##name##_RegisterHookPusherInst; + + DEF_AND_ADD_MASTER_FINDBYKEY_HOOK(AchievementMaster) + DEF_AND_ADD_MASTER_FINDBYKEY_HOOK(ProduceSkillMaster) + DEF_AND_ADD_MASTER_FINDBYKEY_HOOK(FeatureLockMaster) + DEF_AND_ADD_MASTER_FINDBYKEY_HOOK(ProduceCardMaster) + + // 安装 DEF_AND_ADD_MASTER_FINDBYKEY_HOOK 的 hook + void InitMasterHooks(HookInstaller* hookInstaller) { + for (auto& func : g_registerMasterFindByKeyHookFuncs) { + func(hookInstaller); + } + } +*/ + void StartInjectFunctions() { const auto hookInstaller = Plugin::GetInstance().GetHookInstaller(); UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW), @@ -1122,6 +1246,39 @@ namespace GakumasLocal::HookMain { ADD_HOOK(TextField_set_value, Il2cppUtils::GetMethodPointer("UnityEngine.UIElementsModule.dll", "UnityEngine.UIElements", "TextField", "set_value")); + /* SQL 查询相关函数,不好用 + // 下面是 byte[] u8 string 转 std::string 的例子 + auto query = reinterpret_cast*>(mtd); + auto data_ptr = reinterpret_cast(query->GetData()); + std::string qS(data_ptr, data_ptr + lastLength); + + ADD_HOOK(PreparedStatement_ExecuteQuery, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master.SQLite", + "PreparedStatement", "ExecuteQuery", {"System.String"})); + ADD_HOOK(PreparedStatement_ExecuteQuery_u8, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master.SQLite", + "PreparedStatement", "ExecuteQuery", {"*", "*"})); + ADD_HOOK(PreparedStatement_FinalizeStatement, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master.SQLite", + "PreparedStatement", "FinalizeStatement")); + */ + + // ADD_HOOK(EffectGroup_ctor, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", + // "EffectGroup", ".ctor")); + + ADD_HOOK(MessageExtensions_MergeFrom, Il2cppUtils::GetMethodPointer("Google.Protobuf.dll", "Google.Protobuf", + "MessageExtensions", "MergeFrom", {"Google.Protobuf.IMessage", "System.ReadOnlySpan"})); + + /* // 此 block 为 MasterBase 相关的 hook,后来发现它们最后都会调用 MessageExtensions.MergeFrom 进行构造,遂停用。现留档以备用 + // ADD_HOOK(MasterBase_GetAll, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master", + // "MasterBase`2", "GetAll", {"*", "*", "*", "*", "*"})); + + // 安装 DEF_AND_ADD_MASTER_FINDBYKEY_HOOK 的 hook + InitMasterHooks(hookInstaller); + + auto AchievementMaster_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Master", "AchievementMaster"); + auto AchievementMaster_GetAll_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(AchievementMaster_klass->address, "GetAll", 5); + // auto AchievementMaster_FindByKey_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(AchievementMaster_klass->address, "FindByKey", 1); + // Log::DebugFmt("AchievementMaster_GetAll_mtd at %p", AchievementMaster_GetAll_mtd); + ADD_HOOK(MasterBase_GetAll, AchievementMaster_GetAll_mtd->methodPointer); + */ ADD_HOOK(OctoCaching_GetResourceFileName, Il2cppUtils::GetMethodPointer("Octo.dll", "Octo.Caching", "OctoCaching", "GetResourceFileName")); @@ -1304,11 +1461,13 @@ namespace GakumasLocal::HookMain { } Local::LoadData(); + MasterLocal::LoadData(); if (Config::lazyInit) { UnityResolveProgress::classProgress.current = 1; - UnityResolveProgress::startInit = false; + // UnityResolveProgress::startInit = false; } + UnityResolveProgress::startInit = false; Log::Info("Plugin init finished."); return ret; diff --git a/app/src/main/cpp/GakumasLocalify/Il2cppUtils.hpp b/app/src/main/cpp/GakumasLocalify/Il2cppUtils.hpp index 06f33a9..a8c3011 100644 --- a/app/src/main/cpp/GakumasLocalify/Il2cppUtils.hpp +++ b/app/src/main/cpp/GakumasLocalify/Il2cppUtils.hpp @@ -14,28 +14,6 @@ namespace Il2cppUtils { const char* namespaze; }; - struct MethodInfo { - uintptr_t methodPointer; - uintptr_t invoker_method; - const char* name; - uintptr_t klass; - //const Il2CppType* return_type; - //const ParameterInfo* parameters; - const void* return_type; - const void* parameters; - uintptr_t methodDefinition; - uintptr_t genericContainer; - uint32_t token; - uint16_t flags; - uint16_t iflags; - uint16_t slot; - uint8_t parameters_count; - uint8_t is_generic : 1; - uint8_t is_inflated : 1; - uint8_t wrapper_type : 1; - uint8_t is_marshaled_from_native : 1; - }; - struct Il2CppObject { union @@ -110,7 +88,37 @@ namespace Il2cppUtils { int herz; }; - UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName, + struct FieldInfo { + const char* name; + const Il2CppType* type; + uintptr_t parent; + int32_t offset; + uint32_t token; + }; + + struct MethodInfo { + uintptr_t methodPointer; + uintptr_t invoker_method; + const char* name; + uintptr_t klass; + const Il2CppType* return_type; + //const ParameterInfo* parameters; + // const void* return_type; + const void* parameters; + uintptr_t methodDefinition; + uintptr_t genericContainer; + uint32_t token; + uint16_t flags; + uint16_t iflags; + uint16_t slot; + uint8_t parameters_count; + uint8_t is_generic : 1; + uint8_t is_inflated : 1; + uint8_t wrapper_type : 1; + uint8_t is_marshaled_from_native : 1; + }; + + static UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName, const std::string& className) { const auto assembly = UnityResolve::Get(assemblyName); if (!assembly) { @@ -149,7 +157,7 @@ namespace Il2cppUtils { return ret; }*/ - UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName, + static UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName, const std::string& className, const std::string& methodName, const std::vector& args = {}) { const auto assembly = UnityResolve::Get(assemblyName); if (!assembly) { @@ -176,7 +184,7 @@ namespace Il2cppUtils { return method; } - void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName, + static void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName, const std::string& className, const std::string& methodName, const std::vector& args = {}) { auto method = GetMethod(assemblyName, nameSpaceName, className, methodName, args); if (method) { @@ -185,20 +193,19 @@ namespace Il2cppUtils { return nullptr; } - void* il2cpp_resolve_icall(const char* s) { + static void* il2cpp_resolve_icall(const char* s) { return UnityResolve::Invoke("il2cpp_resolve_icall", s); } - Il2CppClassHead* get_class_from_instance(const void* instance) { + static Il2CppClassHead* get_class_from_instance(const void* instance) { return static_cast(*static_cast(std::assume_aligned(instance))); } - MethodInfo* il2cpp_class_get_method_from_name(void* klass, const char* name, int argsCount) { + static MethodInfo* il2cpp_class_get_method_from_name(void* klass, const char* name, int argsCount) { return UnityResolve::Invoke("il2cpp_class_get_method_from_name", klass, name, argsCount); } - void* find_nested_class(void* klass, std::predicate auto&& predicate) - { + static void* find_nested_class(void* klass, std::predicate auto&& predicate) { void* iter{}; while (const auto curNestedClass = UnityResolve::Invoke("il2cpp_class_get_nested_types", klass, &iter)) { @@ -211,24 +218,34 @@ namespace Il2cppUtils { return nullptr; } - void* find_nested_class_from_name(void* klass, const char* name) - { + static void* find_nested_class_from_name(void* klass, const char* name) { return find_nested_class(klass, [name = std::string_view(name)](void* nestedClass) { return static_cast(nestedClass)->name == name; }); } template - auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType { + static auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType { return *reinterpret_cast(reinterpret_cast(obj) + field->offset); } template - auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, RType value) -> void { + static auto ClassGetFieldValue(void* obj, FieldInfo* field) -> RType { + return *reinterpret_cast(reinterpret_cast(obj) + field->offset); + } + + template + static auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, T value) -> void { + const auto fieldPtr = static_cast(obj) + field->offset; + std::memcpy(fieldPtr, std::addressof(value), sizeof(T)); + } + + template + static auto ClassSetFieldValue(void* obj, FieldInfo* field, RType value) -> void { *reinterpret_cast(reinterpret_cast(obj) + field->offset) = value; } - void* get_system_class_from_reflection_type_str(const char* typeStr, const char* assemblyName = "mscorlib") { + static void* get_system_class_from_reflection_type_str(const char* typeStr, const char* assemblyName = "mscorlib") { using Il2CppString = UnityResolve::UnityType::String; static auto assemblyLoad = reinterpret_cast( @@ -245,6 +262,43 @@ namespace Il2cppUtils { return UnityResolve::Invoke("il2cpp_class_from_system_type", reflectionType); } + static std::unordered_map> enumToValueMapCache{}; + static std::unordered_map EnumToValueMap(Il2CppClassHead* enumClass, bool useCache) { + std::unordered_map ret{}; + auto isEnum = UnityResolve::Invoke("il2cpp_class_is_enum", enumClass); + + if (isEnum) { + Il2cppUtils::FieldInfo* field = nullptr; + void* iter = nullptr; + + std::string cacheName = std::string(enumClass->namespaze) + "::" + enumClass->name; + if (useCache) { + if (auto it = enumToValueMapCache.find(cacheName); it != enumToValueMapCache.end()) { + return it->second; + } + } + + while ((field = UnityResolve::Invoke("il2cpp_class_get_fields", enumClass, &iter))) { + // Log::DebugFmt("field: %s, off: %d", field->name, field->offset); + if (field->offset > 0) continue; // 非 static + if (strcmp(field->name, "value__") == 0) { + continue; + } + + int value; + UnityResolve::Invoke("il2cpp_field_static_get_value", field, &value); + // Log::DebugFmt("returnClass: %s - %s: 0x%x", enumClass->name, field->name, value); + std::string itemName = std::string(enumClass->name) + "_" + field->name; + ret.emplace(value, std::move(itemName)); + } + + if (useCache) { + enumToValueMapCache.emplace(std::move(cacheName), ret); + } + } + return ret; + } + namespace Tools { template diff --git a/app/src/main/cpp/GakumasLocalify/Local.h b/app/src/main/cpp/GakumasLocalify/Local.h index 0f14de8..b3cfd82 100644 --- a/app/src/main/cpp/GakumasLocalify/Local.h +++ b/app/src/main/cpp/GakumasLocalify/Local.h @@ -3,8 +3,11 @@ #include #include +#include namespace GakumasLocal::Local { + extern std::unordered_set translatedText; + std::filesystem::path GetBasePath(); void LoadData(); bool GetI18n(const std::string& key, std::string* ret); diff --git a/app/src/main/cpp/GakumasLocalify/MasterLocal.cpp b/app/src/main/cpp/GakumasLocalify/MasterLocal.cpp new file mode 100644 index 0000000..dc793cd --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/MasterLocal.cpp @@ -0,0 +1,807 @@ +#include "MasterLocal.h" +#include "Local.h" +#include "Il2cppUtils.hpp" +#include "config/Config.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GakumasLocal::MasterLocal { + using Il2cppString = UnityResolve::UnityType::String; + + static std::unordered_map fieldSetCache; + static std::unordered_map fieldGetCache; + + enum class JsonValueType { + JVT_String, + JVT_Int, + JVT_Object, + JVT_ArrayObject, + JVT_ArrayString, + JVT_Unsupported, + JVT_NeedMore_EmptyArray + }; + + struct ItemRule { + std::vector mainPrimaryKey; + std::map> subPrimaryKey; + + std::vector mainLocalKey; + std::map> subLocalKey; + }; + + struct TableLocalData { + ItemRule itemRule; + + std::unordered_map mainKeyType; + std::unordered_map> subKeyType; + + std::unordered_map transData; + std::unordered_map> transStrListData; + + [[nodiscard]] JsonValueType GetMainKeyType(const std::string& mainKey) const { + if (auto it = mainKeyType.find(mainKey); it != mainKeyType.end()) { + return it->second; + } + return JsonValueType::JVT_Unsupported; + } + + [[nodiscard]] JsonValueType GetSubKeyType(const std::string& parentKey, const std::string& subKey) const { + if (auto it = subKeyType.find(parentKey); it != subKeyType.end()) { + if (auto subIt = it->second.find(subKey); subIt != it->second.end()) { + return subIt->second; + } + } + return JsonValueType::JVT_Unsupported; + } + }; + + static std::unordered_map masterLocalData; + + class FieldController { + void* self; + std::string self_klass_name; + + static std::string capitalizeFirstLetter(const std::string& input) { + if (input.empty()) return input; + std::string result = input; + result[0] = static_cast(std::toupper(result[0])); + return result; + } + + Il2cppUtils::MethodInfo* GetGetSetMethodFromCache(const std::string& fieldName, int argsCount, + std::unordered_map& fromCache, const std::string& prefix = "set_") { + const std::string methodName = prefix + capitalizeFirstLetter(fieldName); + const std::string searchName = self_klass_name + "." + methodName; + + if (auto it = fromCache.find(searchName); it != fromCache.end()) { + return it->second; + } + auto set_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( + self_klass, + methodName.c_str(), + argsCount + ); + fromCache.emplace(searchName, set_mtd); + return set_mtd; + } + + public: + Il2cppUtils::Il2CppClassHead* self_klass; + + explicit FieldController(void* from) { + if (!from) { + self = nullptr; + return; + } + self = from; + self_klass = Il2cppUtils::get_class_from_instance(self); + if (self_klass) { + self_klass_name = self_klass->name; + } + } + + template + T ReadField(const std::string& fieldName) { + if (!self) return T(); + auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_"); + if (get_mtd) { + return reinterpret_cast(get_mtd->methodPointer)(self, get_mtd); + } + + auto field = UnityResolve::Invoke( + "il2cpp_class_get_field_from_name", + self_klass, + (fieldName + '_').c_str() + ); + if (!field) { + return T(); + } + return Il2cppUtils::ClassGetFieldValue(self, field); + } + + template + void SetField(const std::string& fieldName, T value) { + if (!self) return; + auto set_mtd = GetGetSetMethodFromCache(fieldName, 1, fieldSetCache, "set_"); + if (set_mtd) { + reinterpret_cast( + set_mtd->methodPointer + )(self, value, set_mtd); + return; + } + auto field = UnityResolve::Invoke( + "il2cpp_class_get_field_from_name", + self_klass, + (fieldName + '_').c_str() + ); + if (!field) return; + Il2cppUtils::ClassSetFieldValue(self, field, value); + } + + int ReadIntField(const std::string& fieldName) { + return ReadField(fieldName); + } + + Il2cppString* ReadStringField(const std::string& fieldName) { + if (!self) return nullptr; + auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_"); + if (!get_mtd) { + return ReadField(fieldName); + } + auto returnClass = UnityResolve::Invoke( + "il2cpp_class_from_type", + UnityResolve::Invoke("il2cpp_method_get_return_type", get_mtd) + ); + if (!returnClass) { + return reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + } + auto isEnum = UnityResolve::Invoke("il2cpp_class_is_enum", returnClass); + if (!isEnum) { + return reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + } + auto enumMap = Il2cppUtils::EnumToValueMap(returnClass, true); + auto enumValue = reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + if (auto it = enumMap.find(enumValue); it != enumMap.end()) { + return Il2cppString::New(it->second); + } + return nullptr; + } + + void SetStringField(const std::string& fieldName, const std::string& value) { + if (!self) return; + auto newString = Il2cppString::New(value); + SetField(fieldName, newString); + } + + void SetStringListField(const std::string& fieldName, const std::vector& data) { + if (!self) return; + static auto List_String_klass = Il2cppUtils::get_system_class_from_reflection_type_str( + "System.Collections.Generic.List`1[System.String]" + ); + static auto List_String_ctor_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( + List_String_klass, ".ctor", 0 + ); + static auto List_String_ctor = reinterpret_cast( + List_String_ctor_mtd->methodPointer + ); + + auto newList = UnityResolve::Invoke("il2cpp_object_new", List_String_klass); + List_String_ctor(newList, List_String_ctor_mtd); + + Il2cppUtils::Tools::CSListEditor newListEditor(newList); + for (auto& s : data) { + newListEditor.Add(Il2cppString::New(s)); + } + SetField(fieldName, newList); + } + + void* ReadObjectField(const std::string& fieldName) { + if (!self) return nullptr; + return ReadField(fieldName); + } + + void* ReadObjectListField(const std::string& fieldName) { + if (!self) return nullptr; + return ReadField(fieldName); + } + + static FieldController CreateSubFieldController(void* subObj) { + return FieldController(subObj); + } + + FieldController CreateSubFieldController(const std::string& subObjName) { + auto field = ReadObjectField(subObjName); + return FieldController(field); + } + }; + + + JsonValueType checkJsonValueType(const nlohmann::json& j) { + if (j.is_string()) return JsonValueType::JVT_String; + if (j.is_number_integer()) return JsonValueType::JVT_Int; + if (j.is_object()) return JsonValueType::JVT_Object; + if (j.is_array()) { + if (!j.empty()) { + if (j.begin()->is_object()) { + return JsonValueType::JVT_ArrayObject; + } + else if (j.begin()->is_string()) { + return JsonValueType::JVT_ArrayString; + } + } + else { + return JsonValueType::JVT_NeedMore_EmptyArray; + } + } + return JsonValueType::JVT_Unsupported; + } + + + std::string ReadFileToString(const std::filesystem::path& path) { + std::ifstream ifs(path, std::ios::binary); + if (!ifs) return {}; + std::stringstream buffer; + buffer << ifs.rdbuf(); + return buffer.str(); + } + + namespace Load { + std::vector ArrayStrJsonToVec(nlohmann::json& data) { + return data; + } + + bool BuildObjectItemLocalRule(nlohmann::json& transData, ItemRule& itemRule) { + // transData: data[] + bool hasSuccess = false; + for (auto& data : transData) { + // data: {"id": "xxx", "produceDescriptions": [{"k", "v"}], "descriptions": {"k2", "v2"}} + if (!data.is_object()) continue; + for (auto& [key, value] : data.items()) { + // key: "id", value: "xxx" + // key: "produceDescriptions", value: [{"k", "v"}] + const auto valueType = checkJsonValueType(value); + switch (valueType) { + case JsonValueType::JVT_String: + // case JsonValueType::JVT_Int: + case JsonValueType::JVT_ArrayString: { + if (std::find(itemRule.mainPrimaryKey.begin(), itemRule.mainPrimaryKey.end(), key) != itemRule.mainPrimaryKey.end()) { + continue; + } + if (auto it = std::find(itemRule.mainLocalKey.begin(), itemRule.mainLocalKey.end(), key); it == itemRule.mainLocalKey.end()) { + itemRule.mainLocalKey.emplace_back(key); + } + hasSuccess = true; + } break; + + case JsonValueType::JVT_Object: { + ItemRule currRule{ .mainPrimaryKey = itemRule.subPrimaryKey[key] }; + + auto vJson = nlohmann::json::array(); + vJson.push_back(value); + + if (BuildObjectItemLocalRule(vJson, currRule)) { + itemRule.subLocalKey.emplace(key, currRule.mainLocalKey); + hasSuccess = true; + } + } break; + + case JsonValueType::JVT_ArrayObject: { + for (auto& obj : value) { + // obj: {"k", "v"} + ItemRule currRule{ .mainPrimaryKey = itemRule.subPrimaryKey[key] }; + if (BuildObjectItemLocalRule(value, currRule)) { + itemRule.subLocalKey.emplace(key, currRule.mainLocalKey); + hasSuccess = true; + break; + } + } + } break; + + case JsonValueType::JVT_Unsupported: + default: + break; + } + } + if (hasSuccess) break; + } + return hasSuccess; + } + + bool GetItemRule(nlohmann::json& fullData, ItemRule& itemRule) { + auto& primaryKeys = fullData["rules"]["primaryKeys"]; + auto& transData = fullData["data"]; + if (!primaryKeys.is_array()) return false; + if (!transData.is_array()) return false; + + // 首先构造 mainPrimaryKey 规则 + for (auto& pkItem : primaryKeys) { + if (!pkItem.is_string()) { + return false; + } + std::string pk = pkItem; + auto dotCount = std::ranges::count(pk, '.'); + if (dotCount == 0) { + itemRule.mainPrimaryKey.emplace_back(pk); + } + else if (dotCount == 1) { + auto [parentKey, subKey] = Misc::StringFormat::split_once(pk, "."); + if (itemRule.subPrimaryKey.contains(parentKey)) { + itemRule.subPrimaryKey[parentKey].emplace_back(subKey); + } + else { + itemRule.subPrimaryKey.emplace(parentKey, std::vector{subKey}); + } + } + else { + Log::ErrorFmt("Unsupported depth: %d", dotCount); + continue; + } + } + return BuildObjectItemLocalRule(transData, itemRule); + } + + std::string BuildBaseMainUniqueKey(nlohmann::json& data, TableLocalData& tableLocalData) { + try { + std::string mainBaseUniqueKey; + for (auto& mainPrimaryKey : tableLocalData.itemRule.mainPrimaryKey) { + if (!data.contains(mainPrimaryKey)) { + return ""; + } + auto& value = data[mainPrimaryKey]; + if (value.is_number_integer()) { + mainBaseUniqueKey.append(std::to_string(value.get())); + } + else { + mainBaseUniqueKey.append(value); + } + mainBaseUniqueKey.push_back('|'); + } + return mainBaseUniqueKey; + } + catch (std::exception& e) { + Log::ErrorFmt("LoadData - BuildBaseMainUniqueKey failed: %s", e.what()); + throw e; + } + } + + void BuildBaseObjectSubUniqueKey(nlohmann::json& value, JsonValueType valueType, std::string& currLocalKey) { + switch (valueType) { + case JsonValueType::JVT_String: + currLocalKey.append(value.get()); // p_card-00-acc-0_002|0|produceDescriptions|ProduceDescriptionType_Exam| + currLocalKey.push_back('|'); + break; + case JsonValueType::JVT_Int: + currLocalKey.append(std::to_string(value.get())); + currLocalKey.push_back('|'); + break; + default: + break; + } + } + + bool BuildUniqueKeyValue(nlohmann::json& data, TableLocalData& tableLocalData) { + // 首先处理 main 部分 + const std::string mainBaseUniqueKey = BuildBaseMainUniqueKey(data, tableLocalData); // p_card-00-acc-0_002|0| + if (mainBaseUniqueKey.empty()) return false; + for (auto& mainLocalKey : tableLocalData.itemRule.mainLocalKey) { + if (!data.contains(mainLocalKey)) continue; + auto& currLocalValue = data[mainLocalKey]; + auto currUniqueKey = mainBaseUniqueKey + mainLocalKey; // p_card-00-acc-0_002|0|name + if (tableLocalData.GetMainKeyType(mainLocalKey) == JsonValueType::JVT_ArrayString) { + tableLocalData.transStrListData.emplace(currUniqueKey, ArrayStrJsonToVec(currLocalValue)); + } + else { + tableLocalData.transData.emplace(currUniqueKey, currLocalValue); + } + } + // 然后处理 sub 部分 + /* + for (const auto& [subPrimaryParentKey, subPrimarySubKeys] : tableLocalData.itemRule.subPrimaryKey) { + if (!data.contains(subPrimaryParentKey)) continue; + + const std::string subBaseUniqueKey = mainBaseUniqueKey + subPrimaryParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions| + + auto subValueType = checkJsonValueType(data[subPrimaryParentKey]); + std::string currLocalKey = subBaseUniqueKey; // p_card-00-acc-0_002|0|produceDescriptions| + switch (subValueType) { + case JsonValueType::JVT_Object: { + for (auto& subPrimarySubKey : subPrimarySubKeys) { + if (!data[subPrimaryParentKey].contains(subPrimarySubKey)) continue; + auto& value = data[subPrimaryParentKey][subPrimarySubKey]; + auto valueType = tableLocalData.GetSubKeyType(subPrimaryParentKey, subPrimarySubKey); + BuildBaseObjectSubUniqueKey(value, valueType, currLocalKey); // p_card-00-acc-0_002|0|produceDescriptions|ProduceDescriptionType_Exam| + } + } break; + case JsonValueType::JVT_ArrayObject: { + int currIndex = 0; + for (auto& obj : data[subPrimaryParentKey]) { + for (auto& subPrimarySubKey : subPrimarySubKeys) { + + } + currIndex++; + } + } break; + default: + break; + } + }*/ + + for (const auto& [subLocalParentKey, subLocalSubKeys] : tableLocalData.itemRule.subLocalKey) { + if (!data.contains(subLocalParentKey)) continue; + + const std::string subBaseUniqueKey = mainBaseUniqueKey + subLocalParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions| + auto subValueType = checkJsonValueType(data[subLocalParentKey]); + if (subValueType != JsonValueType::JVT_NeedMore_EmptyArray) { + tableLocalData.mainKeyType.emplace(subLocalParentKey, subValueType); // 在这里插入 subParent 的类型 + } + switch (subValueType) { + case JsonValueType::JVT_Object: { + for (auto& localSubKey : subLocalSubKeys) { + const std::string currLocalUniqueKey = subBaseUniqueKey + localSubKey; // p_card-00-acc-0_002|0|produceDescriptions|text + if (tableLocalData.GetSubKeyType(subLocalParentKey, localSubKey) == JsonValueType::JVT_ArrayString) { + tableLocalData.transStrListData.emplace(currLocalUniqueKey, ArrayStrJsonToVec(data[subLocalParentKey][localSubKey])); + } + else { + tableLocalData.transData.emplace(currLocalUniqueKey, data[subLocalParentKey][localSubKey]); + } + } + } break; + case JsonValueType::JVT_ArrayObject: { + int currIndex = 0; + for (auto& obj : data[subLocalParentKey]) { + for (auto& localSubKey : subLocalSubKeys) { + std::string currLocalUniqueKey = subBaseUniqueKey; // p_card-00-acc-0_002|0|produceDescriptions| + currLocalUniqueKey.push_back('['); + currLocalUniqueKey.append(std::to_string(currIndex)); + currLocalUniqueKey.append("]|"); + currLocalUniqueKey.append(localSubKey); // p_card-00-acc-0_002|0|produceDescriptions|[0]|text + + if (tableLocalData.GetSubKeyType(subLocalParentKey, localSubKey) == JsonValueType::JVT_ArrayString) { + // if (obj[localSubKey].is_array()) { + tableLocalData.transStrListData.emplace(currLocalUniqueKey, ArrayStrJsonToVec(obj[localSubKey])); + } + else if (obj[localSubKey].is_string()) { + tableLocalData.transData.emplace(currLocalUniqueKey, obj[localSubKey]); + } + } + currIndex++; + } + } break; + default: + break; + } + } + return true; + } + +#define MainKeyTypeProcess() if (!data.contains(mainPrimaryKey)) { Log::ErrorFmt("mainPrimaryKey: %s not found", mainPrimaryKey.c_str()); isFailed = true; break; } \ + auto currType = checkJsonValueType(data[mainPrimaryKey]); \ + if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \ + tableLocalData.mainKeyType[mainPrimaryKey] = currType +#define SubKeyTypeProcess() if (!data.contains(subKeyParent)) { Log::ErrorFmt("subKeyParent: %s not found", subKeyParent.c_str()); isFailed = true; break; } \ + for (auto& subKey : subKeys) { \ + auto& subKeyValue = data[subKeyParent]; \ + if (subKeyValue.is_object()) { \ + if (!subKeyValue.contains(subKey)) { \ + Log::ErrorFmt("subKey: %s not in subKeyParent: %s", subKey.c_str(), subKeyParent.c_str()); isFailed = true; break; \ + } \ + auto currType = checkJsonValueType(subKeyValue[subKey]); \ + if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \ + tableLocalData.subKeyType[subKeyParent].emplace(subKey, currType); \ + } \ + else if (subKeyValue.is_array()) { \ + if (subKeyValue.empty()) goto NextLoop; \ + for (auto& i : subKeyValue) { \ + if (!i.is_object()) continue; \ + if (!i.contains(subKey)) continue; \ + auto currType = checkJsonValueType(i[subKey]); \ + if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \ + tableLocalData.subKeyType[subKeyParent].emplace(subKey, currType); \ + break; \ + } \ + } \ + else { \ + goto NextLoop;\ + } \ + } + + bool GetTableLocalData(nlohmann::json& fullData, TableLocalData& tableLocalData) { + bool isFailed = false; + + // 首先 Build mainKeyType 和 subKeyType + for (auto& data : fullData["data"]) { + if (!data.is_object()) continue; + + for (auto& mainPrimaryKey : tableLocalData.itemRule.mainPrimaryKey) { + MainKeyTypeProcess(); + } + for (auto& mainPrimaryKey : tableLocalData.itemRule.mainLocalKey) { + MainKeyTypeProcess(); + } + + for (const auto& [subKeyParent, subKeys] : tableLocalData.itemRule.subPrimaryKey) { + SubKeyTypeProcess() + + if (isFailed) break; + } + for (const auto& [subKeyParent, subKeys] : tableLocalData.itemRule.subLocalKey) { + SubKeyTypeProcess() + if (isFailed) break; + } + if (!isFailed) break; + NextLoop: + } + + if (isFailed) return false; + + bool hasSuccess = false; + // 然后构造 transData + for (auto& data : fullData["data"]) { + if (!data.is_object()) continue; + if (BuildUniqueKeyValue(data, tableLocalData)) { + hasSuccess = true; + } + } + if (!hasSuccess) { + Log::ErrorFmt("BuildUniqueKeyValue failed."); + } + return hasSuccess; + } + + void LoadData() { + masterLocalData.clear(); + static auto masterDir = Local::GetBasePath() / "local-files" / "masterTrans"; + if (!std::filesystem::is_directory(masterDir)) { + Log::ErrorFmt("LoadData: not found: %s", masterDir.string().c_str()); + return; + } + + bool isFirstIteration = true; + for (auto& p : std::filesystem::directory_iterator(masterDir)) { + if (isFirstIteration) { + auto totalFileCount = std::distance( + std::filesystem::directory_iterator(masterDir), + std::filesystem::directory_iterator{} + ); + UnityResolveProgress::classProgress.total = totalFileCount <= 0 ? 1 : totalFileCount; + isFirstIteration = false; + } + UnityResolveProgress::classProgress.current++; + + if (!p.is_regular_file()) continue; + const auto& path = p.path(); + if (path.extension() != ".json") continue; + + std::string tableName = path.stem().string(); + auto fileContent = ReadFileToString(path); + if (fileContent.empty()) continue; + + try { + auto j = nlohmann::json::parse(fileContent); + if (!j.contains("rules") || !j["rules"].contains("primaryKeys")) { + continue; + } + ItemRule currRule; + if (!GetItemRule(j, currRule)) { + Log::ErrorFmt("GetItemRule failed: %s", path.string().c_str()); + continue; + } + + /* + if (tableName == "ProduceStepEventDetail") { + for (auto& i : currRule.mainLocalKey) { + Log::DebugFmt("currRule.mainLocalKey: %s", i.c_str()); + } + for (auto& i : currRule.mainPrimaryKey) { + Log::DebugFmt("currRule.mainPrimaryKey: %s", i.c_str()); + } + for (auto& i : currRule.subLocalKey) { + for (auto& m : i.second) { + Log::DebugFmt("currRule.subLocalKey: %s - %s", i.first.c_str(), m.c_str()); + } + } + for (auto& i : currRule.subPrimaryKey) { + for (auto& m : i.second) { + Log::DebugFmt("currRule.subPrimaryKey: %s - %s", i.first.c_str(), m.c_str()); + } + } + }*/ + + TableLocalData tableLocalData{ .itemRule = currRule }; + if (GetTableLocalData(j, tableLocalData)) { + for (auto& i : tableLocalData.transData) { + // Log::DebugFmt("%s: %s -> %s", tableName.c_str(), i.first.c_str(), i.second.c_str()); + Local::translatedText.emplace(i.second); + } + for (auto& i : tableLocalData.transStrListData) { + for (auto& str : i.second) { + // Log::DebugFmt("%s[]: %s -> %s", tableName.c_str(), i.first.c_str(), str.c_str()); + Local::translatedText.emplace(str); + } + } + + /* + if (tableName == "ProduceStepEventDetail") { + for (auto& i : tableLocalData.mainKeyType) { + Log::DebugFmt("mainKeyType: %s -> %d", i.first.c_str(), i.second); + } + for (auto& i : tableLocalData.subKeyType) { + for (auto& m : i.second) { + Log::DebugFmt("subKeyType: %s - %s -> %d", i.first.c_str(), m.first.c_str(), m.second); + } + } + }*/ + // JVT_ArrayString in HelpCategory, ProduceStory, Tutorial + + masterLocalData.emplace(tableName, std::move(tableLocalData)); + } + else { + Log::ErrorFmt("GetTableLocalData failed: %s", path.string().c_str()); + } + } catch (std::exception& e) { + Log::ErrorFmt("MasterLocal::LoadData: parse error in '%s': %s", + path.string().c_str(), e.what()); + } + } + } + } + + void LoadData() { + return Load::LoadData(); + } + + std::string GetTransString(const std::string& key, const TableLocalData& localData) { + if (auto it = localData.transData.find(key); it != localData.transData.end()) { + return it->second; + } + return {}; + } + + std::vector GetTransArrayString(const std::string& key, const TableLocalData& localData) { + if (auto it = localData.transStrListData.find(key); it != localData.transStrListData.end()) { + return it->second; + } + return {}; + } + + void LocalizeMasterItem(FieldController& fc, const std::string& tableName) { + auto it = masterLocalData.find(tableName); + if (it == masterLocalData.end()) return; + const auto& localData = it->second; + + // 首先拼 BasePrimaryKey + std::string baseDataKey; // p_card-00-acc-0_002|0| + for (auto& mainPk : localData.itemRule.mainPrimaryKey) { + auto mainPkType = localData.GetMainKeyType(mainPk); + switch (mainPkType) { + case JsonValueType::JVT_Int: { + auto readValue = std::to_string(fc.ReadIntField(mainPk)); + baseDataKey.append(readValue); + baseDataKey.push_back('|'); + } break; + case JsonValueType::JVT_String: { + auto readValue = fc.ReadStringField(mainPk); + baseDataKey.append(readValue->ToString()); + baseDataKey.push_back('|'); + } break; + default: + break; + } + } + + // 然后本地化 mainLocal + for (auto& mainLocal : localData.itemRule.mainLocalKey) { + std::string currSearchKey = baseDataKey; + currSearchKey.append(mainLocal); // p_card-00-acc-0_002|0|name + auto localVType = localData.GetMainKeyType(mainLocal); + switch (localVType) { + case JsonValueType::JVT_String: { + auto localValue = GetTransString(currSearchKey, localData); + if (!localValue.empty()) { + fc.SetStringField(mainLocal, localValue); + } + } break; + case JsonValueType::JVT_ArrayString: { + auto localValue = GetTransArrayString(currSearchKey, localData); + if (!localValue.empty()) { + fc.SetStringListField(mainLocal, localValue); + } + } break; + default: + break; + } + } + + // 处理 sub + for (const auto& [subParentKey, subLocalKeys] : localData.itemRule.subLocalKey) { + const auto subBaseSearchKey = baseDataKey + subParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions| + + const auto subParentType = localData.GetMainKeyType(subParentKey); + switch (subParentType) { + case JsonValueType::JVT_Object: { + auto subParentField = fc.CreateSubFieldController(subParentKey); + for (const auto& subLocalKey : subLocalKeys) { + const auto currSearchKey = subBaseSearchKey + subLocalKey; // p_card-00-acc-0_002|0|produceDescriptions|text + auto localKeyType = localData.GetSubKeyType(subParentKey, subLocalKey); + if (localKeyType == JsonValueType::JVT_String) { + auto setData = GetTransString(currSearchKey, localData); + if (!setData.empty()) { + subParentField.SetStringField(subLocalKey, setData); + } + } + else if (localKeyType == JsonValueType::JVT_ArrayString) { + auto setData = GetTransArrayString(currSearchKey, localData); + if (!setData.empty()) { + subParentField.SetStringListField(subLocalKey, setData); + } + } + } + } break; + case JsonValueType::JVT_ArrayObject: { + auto subArrField = fc.ReadObjectListField(subParentKey); + if (!subArrField) continue; + Il2cppUtils::Tools::CSListEditor subListEdit(subArrField); + auto count = subListEdit.get_Count(); + for (int idx = 0; idx < count; idx++) { + auto currItem = subListEdit.get_Item(idx); + if (!currItem) continue; + auto currFc = FieldController::CreateSubFieldController(currItem); + + std::string currSearchBaseKey = subBaseSearchKey; // p_card-00-acc-0_002|0|produceDescriptions| + currSearchBaseKey.push_back('['); + currSearchBaseKey.append(std::to_string(idx)); + currSearchBaseKey.append("]|"); // p_card-00-acc-0_002|0|produceDescriptions|[0]| + + for (const auto& subLocalKey : subLocalKeys) { + std::string currSearchKey = currSearchBaseKey + subLocalKey; // p_card-00-acc-0_002|0|produceDescriptions|[0]|text + + auto localKeyType = localData.GetSubKeyType(subParentKey, subLocalKey); + + /* + if (tableName == "ProduceStepEventDetail") { + Log::DebugFmt("localKeyType: %d currSearchKey: %s", localKeyType, currSearchKey.c_str()); + }*/ + + if (localKeyType == JsonValueType::JVT_String) { + auto setData = GetTransString(currSearchKey, localData); + if (!setData.empty()) { + currFc.SetStringField(subLocalKey, setData); + } + } + else if (localKeyType == JsonValueType::JVT_ArrayString) { + auto setData = GetTransArrayString(currSearchKey, localData); + if (!setData.empty()) { + currFc.SetStringListField(subLocalKey, setData); + } + } + } + } + + } break; + default: + break; + } + } + + } + + void LocalizeMasterItem(void* item, const std::string& tableName) { + if (!Config::useMasterTrans) return; + // Log::DebugFmt("LocalizeMasterItem: %s", tableName.c_str()); + FieldController fc(item); + LocalizeMasterItem(fc, tableName); + } + +} // namespace GakumasLocal::MasterLocal diff --git a/app/src/main/cpp/GakumasLocalify/MasterLocal.h b/app/src/main/cpp/GakumasLocalify/MasterLocal.h new file mode 100644 index 0000000..04117d4 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/MasterLocal.h @@ -0,0 +1,12 @@ +#ifndef GAKUMAS_LOCALIFY_MASTERLOCAL_H +#define GAKUMAS_LOCALIFY_MASTERLOCAL_H + +#include + +namespace GakumasLocal::MasterLocal { + void LoadData(); + + void LocalizeMasterItem(void* item, const std::string& tableName); +} + +#endif //GAKUMAS_LOCALIFY_MASTERLOCAL_H diff --git a/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.cpp b/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.cpp new file mode 100644 index 0000000..b795e1a --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.cpp @@ -0,0 +1,786 @@ +#include "MasterLocal.h" +#include "Local.h" +#include "Il2cppUtils.hpp" +#include "config/Config.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GakumasLocal::MasterLocal { + using Il2cppString = UnityResolve::UnityType::String; + + enum class JsonValueType { + JVT_String, + JVT_Int, + JVT_Object, + JVT_ArrayObject, + }; + + struct PKItem { + std::string topLevel; + std::string subField; + JsonValueType topLevelType; + JsonValueType subFieldType; + }; + + struct TableInfo { + std::vector pkItems; + std::unordered_map dataMap; + }; + + static std::unordered_map g_loadedData; + static std::unordered_map fieldSetCache; + static std::unordered_map fieldGetCache; + + class FieldController { + void* self; + std::string self_klass_name; + + static std::string capitalizeFirstLetter(const std::string& input) { + if (input.empty()) return input; + std::string result = input; + result[0] = static_cast(std::toupper(result[0])); + return result; + } + + Il2cppUtils::MethodInfo* GetGetSetMethodFromCache(const std::string& fieldName, int argsCount, + std::unordered_map& fromCache, const std::string& prefix = "set_") { + const std::string methodName = prefix + capitalizeFirstLetter(fieldName); + const std::string searchName = self_klass_name + "." + methodName; + + if (auto it = fromCache.find(searchName); it != fromCache.end()) { + return it->second; + } + auto set_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( + self_klass, + methodName.c_str(), + argsCount + ); + fromCache.emplace(searchName, set_mtd); + return set_mtd; + } + + public: + Il2cppUtils::Il2CppClassHead* self_klass; + + explicit FieldController(void* from) { + self = from; + self_klass = Il2cppUtils::get_class_from_instance(self); + if (self_klass) { + self_klass_name = self_klass->name; + } + } + + template + T ReadField(const std::string& fieldName) { + auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_"); + if (get_mtd) { + return reinterpret_cast(get_mtd->methodPointer)(self, get_mtd); + } + + auto field = UnityResolve::Invoke( + "il2cpp_class_get_field_from_name", + self_klass, + (fieldName + '_').c_str() + ); + if (!field) { + return T(); + } + return Il2cppUtils::ClassGetFieldValue(self, field); + } + + template + void SetField(const std::string& fieldName, T value) { + auto set_mtd = GetGetSetMethodFromCache(fieldName, 1, fieldSetCache, "set_"); + if (set_mtd) { + reinterpret_cast( + set_mtd->methodPointer + )(self, value, set_mtd); + return; + } + auto field = UnityResolve::Invoke( + "il2cpp_class_get_field_from_name", + self_klass, + (fieldName + '_').c_str() + ); + if (!field) return; + Il2cppUtils::ClassSetFieldValue(self, field, value); + } + + int ReadIntField(const std::string& fieldName) { + return ReadField(fieldName); + } + + Il2cppString* ReadStringField(const std::string& fieldName) { + auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_"); + if (!get_mtd) { + return ReadField(fieldName); + } + auto returnClass = UnityResolve::Invoke( + "il2cpp_class_from_type", + UnityResolve::Invoke("il2cpp_method_get_return_type", get_mtd) + ); + if (!returnClass) { + return reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + } + auto isEnum = UnityResolve::Invoke("il2cpp_class_is_enum", returnClass); + if (!isEnum) { + return reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + } + auto enumMap = Il2cppUtils::EnumToValueMap(returnClass, true); + auto enumValue = reinterpret_cast( + get_mtd->methodPointer + )(self, get_mtd); + if (auto it = enumMap.find(enumValue); it != enumMap.end()) { + return Il2cppString::New(it->second); + } + return nullptr; + } + + void SetStringField(const std::string& fieldName, const std::string& value) { + auto newString = Il2cppString::New(value); + SetField(fieldName, newString); + } + + void SetStringListField(const std::string& fieldName, const std::vector& data) { + static auto List_String_klass = Il2cppUtils::get_system_class_from_reflection_type_str( + "System.Collections.Generic.List`1[System.String]" + ); + static auto List_String_ctor_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( + List_String_klass, ".ctor", 0 + ); + static auto List_String_ctor = reinterpret_cast( + List_String_ctor_mtd->methodPointer + ); + + auto newList = UnityResolve::Invoke("il2cpp_object_new", List_String_klass); + List_String_ctor(newList, List_String_ctor_mtd); + + Il2cppUtils::Tools::CSListEditor newListEditor(newList); + for (auto& s : data) { + newListEditor.Add(Il2cppString::New(s)); + } + SetField(fieldName, newList); + } + + void* ReadObjectField(const std::string& fieldName) { + return ReadField(fieldName); + } + + void* ReadObjectListField(const std::string& fieldName) { + return ReadField(fieldName); + } + + static FieldController CreateSubFieldController(void* subObj) { + return FieldController(subObj); + } + }; + + //============================================================== + // 帮助函数:判断 JSON 字段类型 + //============================================================== + JsonValueType checkJsonValueType(const nlohmann::json& j) { + if (j.is_string()) return JsonValueType::JVT_String; + if (j.is_number_integer()) return JsonValueType::JVT_Int; + if (j.is_object()) return JsonValueType::JVT_Object; + if (j.is_array()) { + if (!j.empty() && j.begin()->is_object()) { + return JsonValueType::JVT_ArrayObject; + } + } + return JsonValueType::JVT_String; + } + + //============================================================== + // 解析 pkName => PKItem + //============================================================== + PKItem parsePK(const nlohmann::json& row, const std::string& pkStr) { + auto pos = pkStr.find('.'); + PKItem item; + if (pos == std::string::npos) { + item.topLevel = pkStr; + item.subField = ""; + if (!row.contains(pkStr)) { + item.topLevelType = JsonValueType::JVT_String; + } else { + item.topLevelType = checkJsonValueType(row[pkStr]); + } + item.subFieldType = JsonValueType::JVT_String; + } else { + item.topLevel = pkStr.substr(0, pos); + item.subField = pkStr.substr(pos + 1); + if (!row.contains(item.topLevel)) { + item.topLevelType = JsonValueType::JVT_Object; + } else { + auto& jTop = row[item.topLevel]; + auto t = checkJsonValueType(jTop); + if (t == JsonValueType::JVT_Object) { + item.topLevelType = JsonValueType::JVT_Object; + } else if (t == JsonValueType::JVT_ArrayObject) { + item.topLevelType = JsonValueType::JVT_ArrayObject; + } else { + item.topLevelType = JsonValueType::JVT_Object; + } + } + item.subFieldType = JsonValueType::JVT_String; + if (row.contains(item.topLevel)) { + auto& jTop = row[item.topLevel]; + if (jTop.is_object()) { + if (jTop.contains(item.subField)) { + item.subFieldType = checkJsonValueType(jTop[item.subField]); + } + } else if (jTop.is_array() && !jTop.empty()) { + auto& firstElem = *jTop.begin(); + if (firstElem.is_object() && firstElem.contains(item.subField)) { + item.subFieldType = checkJsonValueType(firstElem[item.subField]); + } + } + } + } + return item; + } + + std::vector parseAllPKItems(const nlohmann::json& row, const std::vector& pkNames) { + std::vector result; + result.reserve(pkNames.size()); + for (auto& pk : pkNames) { + auto item = parsePK(row, pk); + result.push_back(item); + } + return result; + } + + //============================================================== + // 将 jval 拼接到 uniqueKey + //============================================================== + inline void appendPKValue(std::string& uniqueKey, const nlohmann::json& jval, bool& isFirst) { + if (!isFirst) uniqueKey += "|"; + if (jval.is_string()) { + uniqueKey += jval.get(); + } else if (jval.is_number_integer()) { + uniqueKey += std::to_string(jval.get()); + } + isFirst = false; + } + + //============================================================== + // 读取文件 => 解析 => 加载 dataMap + //============================================================== + std::string ReadFileToString(const std::filesystem::path& path) { + std::ifstream ifs(path, std::ios::binary); + if (!ifs) return {}; + std::stringstream buffer; + buffer << ifs.rdbuf(); + return buffer.str(); + } + + // 判断 row 里,与 pkNames/主键相关的字段(若是数组)是否为空 + bool hasEmptyArrayForPk(const nlohmann::json& row, const std::vector& pkNames) { + // 如果行为空,直接返回 false(或 true,看你需求) + if (row.is_null() || !row.is_object()) { + return false; + } + + for (auto& pk : pkNames) { + // 先看该行是否包含此顶层字段 + auto dotPos = pk.find('.'); + std::string topLevel = (dotPos == std::string::npos) ? pk : pk.substr(0, dotPos); + + if (!row.contains(topLevel)) { + // 没有这个字段就略过 + continue; + } + + // 如果 pk 中含 '.', 说明可能是 array 类型 + // 这里仅检查 "顶层字段是否是空数组" + // 若需要更深层的判断,需扩展 + const auto& jTop = row[topLevel]; + if (jTop.is_array()) { + // 一旦发现是空数组,就返回 true + if (jTop.empty()) { + return true; + } + } + } + + return false; + } + + // 根据 pkItems 构造一个 skipSet,里面包含 "topLevel" 和 "topLevel.subField" +// 或者只包含 subField, 看你具体需求 + static std::unordered_set buildSkipFields(const std::vector& pkItems) { + std::unordered_set skipSet; + for (auto& pk : pkItems) { + if (pk.subField.empty()) { + // e.g. "id" + skipSet.insert(pk.topLevel); + } else { + // e.g. "descriptions.type" => 既要跳过 "type" 又要跳过 "descriptions"? + // 具体看你业务需要: + // skipSet.insert(pk.topLevel); // 可能不需要 + skipSet.insert(pk.subField); // "type" + } + } + return skipSet; + } + + // 递归枚举 JSON 值里的字符串并插入到 localSet + void collectLocalizableStrings_impl( + const nlohmann::json& node, + const std::unordered_set& skipSet, + std::unordered_set& localSet + ) { + if (node.is_string()) { + // node本身就是string => 这时无法知道key名,但一般情况下我们是key->value对? + // 这里仅当外层调用传入一个object时可取到key + // 先写成仅object字段时处理 + return; + } + if (node.is_object()) { + // 枚举键值 + for (auto it = node.begin(); it != node.end(); ++it) { + auto& key = it.key(); + auto& val = it.value(); + // 如果key在skipSet里,则跳过 + if (skipSet.count(key)) { + continue; + } + // 否则看val的类型 + if (val.is_string()) { + // 收集 + localSet.insert(val.get()); + // Log::DebugFmt("localSet.insert: %s", val.get().c_str()); + } else if (val.is_object() || val.is_array()) { + // 递归下去 + collectLocalizableStrings_impl(val, skipSet, localSet); + } + // 其他类型 (int/bool/float) 不做本地化 + } + } else if (node.is_array()) { + // 枚举数组元素 + for (auto& element : node) { + if (element.is_string()) { + localSet.insert(element.get()); + // Log::DebugFmt("localSet.insert: %s", element.get().c_str()); + } else if (element.is_object() || element.is_array()) { + collectLocalizableStrings_impl(element, skipSet, localSet); + } + } + } + } + + // 对外接口:根据 row + pkItems,把所有非主键字段的字符串插到 localSet + void collectLocalizableStrings(const nlohmann::json& row, const std::vector& pkItems, std::unordered_set& localSet) { + // 先构建一个 skipSet,表示"主键字段"要跳过 + auto skipSet = buildSkipFields(pkItems); + // 然后递归遍历 + collectLocalizableStrings_impl(row, skipSet, localSet); + } + + + void LoadData() { + g_loadedData.clear(); + static auto masterDir = Local::GetBasePath() / "local-files" / "masterTrans"; + if (!std::filesystem::is_directory(masterDir)) { + Log::ErrorFmt("LoadData: not found: %s", masterDir.string().c_str()); + return; + } + + bool isFirstIteration = true; + for (auto& p : std::filesystem::directory_iterator(masterDir)) { + if (isFirstIteration) { + auto totalFileCount = std::distance( + std::filesystem::directory_iterator(masterDir), + std::filesystem::directory_iterator{} + ); + UnityResolveProgress::classProgress.total = totalFileCount <= 0 ? 1 : totalFileCount; + isFirstIteration = false; + } + UnityResolveProgress::classProgress.current++; + + if (!p.is_regular_file()) continue; + const auto& path = p.path(); + if (path.extension() != ".json") continue; + + std::string tableName = path.stem().string(); + auto fileContent = ReadFileToString(path); + if (fileContent.empty()) continue; + + try { + auto j = nlohmann::json::parse(fileContent); + if (!j.contains("rules") || !j["rules"].contains("primaryKeys")) { + continue; + } + std::vector pkNames; + for (auto& x : j["rules"]["primaryKeys"]) { + pkNames.push_back(x.get()); + } + if (!j.contains("data") || !j["data"].is_array()) { + continue; + } + + TableInfo tableInfo; + if (!j["data"].empty()) { + for (auto & currRow : j["data"]) { + if (!hasEmptyArrayForPk(currRow, pkNames)) { + tableInfo.pkItems = parseAllPKItems(currRow, pkNames); + } + } + // auto& firstRow = j["data"][0]; + // tableInfo.pkItems = parseAllPKItems(firstRow, pkNames); + } + + //============================================================== + // 构建 dataMap, 支持 array + index + //============================================================== + for (auto& row : j["data"]) { + std::string uniqueKey; + bool firstKey = true; + bool failed = false; + + for (auto& pkItem : tableInfo.pkItems) { + if (!row.contains(pkItem.topLevel)) { + failed = true; + break; + } + auto& jTop = row[pkItem.topLevel]; + + // 无子字段 => 直接处理 + if (pkItem.subField.empty()) { + if (jTop.is_string() || jTop.is_number_integer()) { + appendPKValue(uniqueKey, jTop, firstKey); + } else { + failed = true; break; + } + } + else { + // 若是 array + subField,就遍历数组每个下标 + subField => 并将 index + value 拼进 uniqueKey + if (pkItem.topLevelType == JsonValueType::JVT_ArrayObject) { + if (!jTop.is_array()) { failed = true; break; } + // 遍历数组所有元素 + for (int i = 0; i < (int)jTop.size(); i++) { + auto& elem = jTop[i]; + if (!elem.is_object()) { failed = true; break; } + if (!elem.contains(pkItem.subField)) { failed = true; break; } + auto& subVal = elem[pkItem.subField]; + // 只支持 string/int + if (!subVal.is_string() && !subVal.is_number_integer()) { + failed = true; break; + } + // 拼上索引 + 值 + // e.g. "|0:xxx|1:yyy"... + if (!firstKey) uniqueKey += "|"; + uniqueKey += std::to_string(i); + uniqueKey += ":"; + if (subVal.is_string()) { + uniqueKey += subVal.get(); + } else { + uniqueKey += std::to_string(subVal.get()); + } + firstKey = false; + } + if (failed) break; + } + else if (pkItem.topLevelType == JsonValueType::JVT_Object) { + if (!jTop.is_object()) { + failed = true; + break; + } + if (!jTop.contains(pkItem.subField)) { failed = true; break; } + auto& subVal = jTop[pkItem.subField]; + if (subVal.is_string() || subVal.is_number_integer()) { + appendPKValue(uniqueKey, subVal, firstKey); + } else { + failed = true; break; + } + } + else { + failed = true; + break; + } + } + if (failed) break; + } + if (!failed && !uniqueKey.empty()) { + tableInfo.dataMap[uniqueKey] = row; + collectLocalizableStrings(row, tableInfo.pkItems, Local::translatedText); + } + } + + // Log::DebugFmt("Load table: %s, %d, %d", tableName.c_str(), tableInfo.pkItems.size(), tableInfo.dataMap.size()); + g_loadedData[tableName] = std::move(tableInfo); + + } catch (std::exception& e) { + Log::ErrorFmt("MasterLocal::LoadData: parse error in '%s': %s", + path.string().c_str(), e.what()); + } + } + } + + //============================================================== + // 在 C# 对象里,根据 pkItems 构造 uniqueKey + // 同样要支持 array + index + //============================================================== + bool buildUniqueKeyFromCSharp(FieldController& fc, const TableInfo& tableInfo, std::string& outKey) { + outKey.clear(); + bool firstKey = true; + + for (auto& pk : tableInfo.pkItems) { + if (pk.subField.empty()) { + // 顶层无子字段 + if (pk.topLevelType == JsonValueType::JVT_String) { + auto sptr = fc.ReadStringField(pk.topLevel); + if (!sptr) return false; + if (!firstKey) outKey += "|"; + outKey += sptr->ToString(); + firstKey = false; + } else if (pk.topLevelType == JsonValueType::JVT_Int) { + int ival = fc.ReadIntField(pk.topLevel); + if (!firstKey) outKey += "|"; + outKey += std::to_string(ival); + firstKey = false; + } else { + return false; + } + } + else { + // subField + if (pk.topLevelType == JsonValueType::JVT_ArrayObject) { + // => c# 里 readObjectListField + void* listPtr = fc.ReadObjectListField(pk.topLevel); + if (!listPtr) return false; + Il2cppUtils::Tools::CSListEditor listEdit(listPtr); + int arrCount = listEdit.get_Count(); + + // 遍历每个 index + for (int i = 0; i < arrCount; i++) { + auto elemPtr = listEdit.get_Item(i); + if (!elemPtr) return false; + FieldController subFC = FieldController::CreateSubFieldController(elemPtr); + + // 只支持 string/int + if (pk.subFieldType == JsonValueType::JVT_String) { + auto sptr = subFC.ReadStringField(pk.subField); + if (!sptr) return false; + if (!firstKey) outKey += "|"; + // "|i:xxx" + outKey += std::to_string(i); + outKey += ":"; + outKey += sptr->ToString(); + firstKey = false; + } + else if (pk.subFieldType == JsonValueType::JVT_Int) { + int ival = subFC.ReadIntField(pk.subField); + if (!firstKey) outKey += "|"; + outKey += std::to_string(i); + outKey += ":"; + outKey += std::to_string(ival); + firstKey = false; + } else { + return false; + } + } + } + else if (pk.topLevelType == JsonValueType::JVT_Object) { + void* subObj = fc.ReadObjectField(pk.topLevel); + if (!subObj) return false; + FieldController subFC = FieldController::CreateSubFieldController(subObj); + + if (pk.subFieldType == JsonValueType::JVT_String) { + auto sptr = subFC.ReadStringField(pk.subField); + if (!sptr) return false; + if (!firstKey) outKey += "|"; + outKey += sptr->ToString(); + firstKey = false; + } + else if (pk.subFieldType == JsonValueType::JVT_Int) { + int ival = subFC.ReadIntField(pk.subField); + if (!firstKey) outKey += "|"; + outKey += std::to_string(ival); + firstKey = false; + } + else { + return false; + } + } + else { + return false; + } + } + } + return !outKey.empty(); + } + + // 声明 + void localizeJsonToCsharp(FieldController& fc, const nlohmann::json& jdata, const std::unordered_set& skipKeySet); + void localizeArrayOfObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& arrVal, const std::unordered_set& skipKeySet); + void localizeObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& objVal, const std::unordered_set& skipKeySet); + + //==================================================================== + // 对 array 做一层递归 —— 需要带着 skipKeySet + //==================================================================== + void localizeArrayOfObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& arrVal, const std::unordered_set& skipKeySet) { + void* listPtr = fc.ReadObjectListField(fieldName); + if (!listPtr) return; + Il2cppUtils::Tools::CSListEditor listEdit(listPtr); + int cmin = std::min(listEdit.get_Count(), (int)arrVal.size()); + for (int i = 0; i < cmin; i++) { + auto elemPtr = listEdit.get_Item(i); + if (!elemPtr) continue; + FieldController subFC = FieldController::CreateSubFieldController(elemPtr); + localizeJsonToCsharp(subFC, arrVal[i], skipKeySet); + } + } + + //==================================================================== + // 对单个 object 做一层递归 —— 需要带着 skipKeySet + //==================================================================== + void localizeObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& objVal, const std::unordered_set& skipKeySet) { + void* subObj = fc.ReadObjectField(fieldName); + if (!subObj) return; + FieldController subFC = FieldController::CreateSubFieldController(subObj); + localizeJsonToCsharp(subFC, objVal, skipKeySet); + } + + //==================================================================== + // 仅一层本地化: string, string[], object, object[],带 skipKeySet + //==================================================================== + void localizeJsonToCsharp(FieldController& fc, const nlohmann::json& jdata, const std::unordered_set& skipKeySet) { + if (!jdata.is_object()) return; + for (auto it = jdata.begin(); it != jdata.end(); ++it) { + const std::string& key = it.key(); + // 如果 key 在 skipKeySet 里,则跳过本地化 + if (skipKeySet.count(key)) { + // Debug输出可以留意一下 + // Log::DebugFmt("skip field: %s", key.c_str()); + continue; + } + + const auto& val = it.value(); + if (val.is_string()) { + // 打印一下做验证 + auto origStr = fc.ReadStringField(key); + auto newStr = val.get(); + if (origStr) { + std::string oldVal = origStr->ToString(); + // Log::DebugFmt("SetStringField key: %s, oldVal: %s -> newVal: %s", key.c_str(), oldVal.c_str(), newStr.c_str()); + if (((oldVal == "\n") || (oldVal == "\r\n")) && newStr.empty()) { + continue; + } + } + fc.SetStringField(key, val.get()); + } + else if (val.is_array()) { + if (!val.empty() && val.begin()->is_string()) { + bool allStr = true; + std::vector strArray; + for (auto& x : val) { + if (!x.is_string()) { allStr = false; break; } + strArray.push_back(x.get()); + } + if (allStr) { + // Log::DebugFmt("SetStringListField in %s, key: %s", fc.self_klass->name, key.c_str()); + fc.SetStringListField(key, strArray); + continue; + } + } + // array + if (!val.empty() && val.begin()->is_object()) { + localizeArrayOfObject(fc, key, val, skipKeySet); + } + } + else if (val.is_object()) { + localizeObject(fc, key, val, skipKeySet); + } + } + } + + //==================================================================== + // 真正处理单个C#对象 + //==================================================================== + void LocalizeMasterItem(FieldController& fc, const std::string& tableName) { + auto it = g_loadedData.find(tableName); + if (it == g_loadedData.end()) return; + // Log::DebugFmt("LocalizeMasterItem: %s", tableName.c_str()); + auto& tableInfo = it->second; + if (tableInfo.dataMap.empty()) { + return; + } + + std::string uniqueKey; + if (!buildUniqueKeyFromCSharp(fc, tableInfo, uniqueKey)) { + return; + } + + auto itRow = tableInfo.dataMap.find(uniqueKey); + if (itRow == tableInfo.dataMap.end()) { + return; + } + const auto& rowData = itRow->second; + + //===================================================== + // 把「有子字段」的 pkItem 也加入 skipKeySet,但用它的 `subField` 部分 + //===================================================== + std::unordered_set skipKeySet; + for (auto& pk : tableInfo.pkItems) { + if (pk.subField.empty()) { + // 若没有子字段,说明 topLevel 本身是主键 + skipKeySet.insert(pk.topLevel); + } else { + // 如果有子字段,说明这个子字段才是 PK + // e.g. produceDescriptions.examEffectType => skipKeySet.insert("examEffectType"); + skipKeySet.insert(pk.subField); + } + } + + // 然后带着 skipKeySet 去做本地化 + localizeJsonToCsharp(fc, rowData, skipKeySet); + } + + void LocalizeMasterTables(const std::string& tableName, UnityResolve::UnityType::List* result) { + if (!result) return; + Il2cppUtils::Tools::CSListEditor resultList(result); + if (resultList.get_Count() <= 0) return; + + for (auto i : resultList) { + if (!i) continue; + FieldController fc(i); + LocalizeMasterItem(fc, tableName); + } + } + + void LocalizeMaster(const std::string& sql, UnityResolve::UnityType::List* result) { + static const std::regex tableNameRegex(R"(\bFROM\s+(?:`([^`]+)`|(\S+)))"); + std::smatch match; + if (std::regex_search(sql, match, tableNameRegex)) { + std::string tableName = match[1].matched ? match[1].str() : match[2].str(); + LocalizeMasterTables(tableName, result); + } + } + + void LocalizeMaster(const std::string& sql, void* result) { + if (!Config::useMasterTrans) return; + LocalizeMaster(sql, reinterpret_cast*>(result)); + } + + void LocalizeMaster(void* result, const std::string& tableName) { + if (!Config::useMasterTrans) return; + LocalizeMasterTables(tableName, reinterpret_cast*>(result)); + } + + void LocalizeMasterItem(void* item, const std::string& tableName) { + if (!Config::useMasterTrans) return; + FieldController fc(item); + LocalizeMasterItem(fc, tableName); + } + +} // namespace GakumasLocal::MasterLocal diff --git a/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.h b/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.h new file mode 100644 index 0000000..973c865 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/MasterLocal_Legacy.h @@ -0,0 +1,15 @@ +#ifndef GAKUMAS_LOCALIFY_MASTERLOCAL_H +#define GAKUMAS_LOCALIFY_MASTERLOCAL_H + +/* +#include + +namespace GakumasLocal::MasterLocal { + void LoadData(); + void LocalizeMaster(const std::string& sql, void* result); + void LocalizeMaster(void* result, const std::string& tableName); + void LocalizeMasterItem(void* item, const std::string& tableName); +} + */ + +#endif //GAKUMAS_LOCALIFY_MASTERLOCAL_H diff --git a/app/src/main/cpp/GakumasLocalify/Misc.cpp b/app/src/main/cpp/GakumasLocalify/Misc.cpp index 5202a32..609d772 100644 --- a/app/src/main/cpp/GakumasLocalify/Misc.cpp +++ b/app/src/main/cpp/GakumasLocalify/Misc.cpp @@ -168,6 +168,33 @@ namespace GakumasLocal::Misc { return fmt; } } + + std::vector split(const std::string& str, char delimiter) { + std::vector result; + std::string current; + for (char c : str) { + if (c == delimiter) { + if (!current.empty()) { + result.push_back(current); + } + current.clear(); + } else { + current += c; + } + } + if (!current.empty()) { + result.push_back(current); + } + return result; + } + + std::pair split_once(const std::string& str, const std::string& delimiter) { + size_t pos = str.find(delimiter); + if (pos != std::string::npos) { + return {str.substr(0, pos), str.substr(pos + delimiter.size())}; + } + return {str, ""}; + } } } diff --git a/app/src/main/cpp/GakumasLocalify/Misc.hpp b/app/src/main/cpp/GakumasLocalify/Misc.hpp index 44ac4eb..a0d97ee 100644 --- a/app/src/main/cpp/GakumasLocalify/Misc.hpp +++ b/app/src/main/cpp/GakumasLocalify/Misc.hpp @@ -76,6 +76,8 @@ namespace GakumasLocal { namespace StringFormat { std::string stringFormatString(const std::string& fmt, const std::vector& vec); + std::vector split(const std::string& str, char delimiter); + std::pair split_once(const std::string& str, const std::string& delimiter); } } } diff --git a/app/src/main/cpp/GakumasLocalify/config/Config.cpp b/app/src/main/cpp/GakumasLocalify/config/Config.cpp index f262581..913ebc9 100644 --- a/app/src/main/cpp/GakumasLocalify/config/Config.cpp +++ b/app/src/main/cpp/GakumasLocalify/config/Config.cpp @@ -11,6 +11,7 @@ namespace GakumasLocal::Config { bool replaceFont = true; bool forceExportResource = true; bool textTest = false; + bool useMasterTrans = true; int gameOrientation = 0; bool dumpText = false; bool enableFreeCamera = false; @@ -64,6 +65,7 @@ namespace GakumasLocal::Config { GetConfigItem(forceExportResource); GetConfigItem(gameOrientation); GetConfigItem(textTest); + GetConfigItem(useMasterTrans); GetConfigItem(dumpText); GetConfigItem(targetFrameRate); GetConfigItem(enableFreeCamera); diff --git a/app/src/main/cpp/GakumasLocalify/config/Config.hpp b/app/src/main/cpp/GakumasLocalify/config/Config.hpp index 5020e16..f052454 100644 --- a/app/src/main/cpp/GakumasLocalify/config/Config.hpp +++ b/app/src/main/cpp/GakumasLocalify/config/Config.hpp @@ -10,6 +10,7 @@ namespace GakumasLocal::Config { extern bool forceExportResource; extern int gameOrientation; extern bool textTest; + extern bool useMasterTrans; extern bool dumpText; extern bool enableFreeCamera; extern int targetFrameRate; diff --git a/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp b/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp index 7252ca2..3e4d9bf 100644 --- a/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp +++ b/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp @@ -309,7 +309,7 @@ public: pDomain = Invoke("il2cpp_domain_get"); Invoke("il2cpp_thread_attach", pDomain); ForeachAssembly(); - if (!lazyInit) UnityResolveProgress::startInit = false; + // if (!lazyInit) UnityResolveProgress::startInit = false; } else { pDomain = Invoke("mono_get_root_domain"); 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 23c2b87..e1ed009 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 @@ -19,6 +19,7 @@ interface ConfigListener { fun onForceExportResourceChanged(value: Boolean) fun onLoginAsIOSChanged(value: Boolean) fun onTextTestChanged(value: Boolean) + fun onUseMasterTransChanged(value: Boolean) fun onReplaceFontChanged(value: Boolean) fun onLazyInitChanged(value: Boolean) fun onEnableFreeCameraChanged(value: Boolean) @@ -138,6 +139,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems { saveConfig() } + override fun onUseMasterTransChanged(value: Boolean) { + config.useMasterTrans = value + saveConfig() + } + override fun onDumpTextChanged(value: Boolean) { config.dumpText = value saveConfig() 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 2bea30e..ccae7d7 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 @@ -146,6 +146,7 @@ object FilesChecker { val genericTransDir = File(localFilesDir, "genericTrans") val genericTransFile = File(localFilesDir, "generic.json") val i18nFile = File(localFilesDir, "localization.json") + val masterTransDir = File(localFilesDir, "masterTrans") if (fontFile.exists()) { fontFile.delete() @@ -156,6 +157,9 @@ object FilesChecker { if (deleteRecursively(genericTransDir)) { genericTransDir.mkdirs() } + if (deleteRecursively(masterTransDir)) { + masterTransDir.mkdirs() + } if (genericTransFile.exists()) { genericTransFile.writeText("{}") } 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 e259fd8..d390abf 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 @@ -9,6 +9,7 @@ data class GakumasConfig ( var lazyInit: Boolean = true, var replaceFont: Boolean = true, var textTest: Boolean = false, + var useMasterTrans: Boolean = true, var dumpText: Boolean = false, var gameOrientation: Int = 0, var forceExportResource: Boolean = false, 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 8a41691..cfc8e07 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 @@ -75,6 +75,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier, item { GakuGroupBox(modifier, stringResource(R.string.debug_settings)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + GakuSwitch(modifier, stringResource(R.string.useMasterDBTrans), checked = config.value.useMasterTrans) { + v -> context?.onUseMasterTransChanged(v) + } + GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) { v -> context?.onTextTestChanged(v) } diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2ca9d6a..e0190e6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -115,6 +115,7 @@ %1$d パーセント。 テストモード - ライブ テキストフックテストモード + MasterDB をローカライズする 翻訳のリポジトリ 翻訳リソースをアップデート すべてのライブを開放 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 029927d..f83377e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -15,6 +15,7 @@ 使用自定义画质设置 RenderScale (0.5/0.59/0.67/0.77/1.0) 文本 hook 测试模式 + 使用 MasterDB 本地化 导出文本 启动后强制导出资源 以 iOS 登陆 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76ace6f..20ec2db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Use Custom Graphics Settings RenderScale (0.5/0.59/0.67/0.77/1.0) Text Hook Test Mode + Enable MasterDB Localization Export Text Force Update Resource Login as iOS diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ee5cec..71f1e97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ lifecycle = "2.8.2" material = "1.12.0" navigationCompose = "2.7.7" xdl = "2.1.1" -shadowhook = "1.0.9" +shadowhook = "1.0.10" serialization="1.7.1" zip4j = "2.9.1"