forked from chinosk/gkms-local
				
			Adapted for multi-platform compilation
Fixed URL input could only enter numbers
This commit is contained in:
		
							parent
							
								
									ade47131f9
								
							
						
					
					
						commit
						d83854c755
					
				| 
						 | 
				
			
			@ -15,8 +15,8 @@ android {
 | 
			
		|||
        applicationId "io.github.chinosk.gakumas.localify"
 | 
			
		||||
        minSdk 29
 | 
			
		||||
        targetSdk 34
 | 
			
		||||
        versionCode 11
 | 
			
		||||
        versionName "v2.1.0"
 | 
			
		||||
        versionCode 12
 | 
			
		||||
        versionName "v3.0.0"
 | 
			
		||||
        buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
 | 
			
		||||
 | 
			
		||||
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
#include <android/log.h>
 | 
			
		||||
#include "Hook.h"
 | 
			
		||||
#include "Plugin.h"
 | 
			
		||||
#include "Log.h"
 | 
			
		||||
| 
						 | 
				
			
			@ -9,14 +8,20 @@
 | 
			
		|||
#include <unordered_set>
 | 
			
		||||
#include "camera/camera.hpp"
 | 
			
		||||
#include "config/Config.hpp"
 | 
			
		||||
#include "shadowhook.h"
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
// #include <jni.h>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <set>
 | 
			
		||||
#include "../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
    #include "../windowsPlatform.hpp"
 | 
			
		||||
    #include "cpprest/details/http_helpers.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
std::unordered_set<void*> hookedStubs{};
 | 
			
		||||
extern std::filesystem::path gakumasLocalPath;
 | 
			
		||||
 | 
			
		||||
#define DEFINE_HOOK(returnType, name, params)                                                      \
 | 
			
		||||
	using name##_Type = returnType(*) params;                                                      \
 | 
			
		||||
| 
						 | 
				
			
			@ -24,26 +29,7 @@ std::unordered_set<void*> hookedStubs{};
 | 
			
		|||
	name##_Type name##_Orig = nullptr;                                                             \
 | 
			
		||||
	returnType name##_Hook params
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#define ADD_HOOK(name, addr)                                                                       \
 | 
			
		||||
	name##_Addr = reinterpret_cast<name##_Type>(addr);                                             \
 | 
			
		||||
	if (addr) {                                                                                    \
 | 
			
		||||
    	auto stub = hookInstaller->InstallHook(reinterpret_cast<void*>(addr),                      \
 | 
			
		||||
                                               reinterpret_cast<void*>(name##_Hook),               \
 | 
			
		||||
                                               reinterpret_cast<void**>(&name##_Orig));            \
 | 
			
		||||
        if (stub == NULL) {                                                                        \
 | 
			
		||||
            int error_num = shadowhook_get_errno();                                                \
 | 
			
		||||
            const char *error_msg = shadowhook_to_errmsg(error_num);                               \
 | 
			
		||||
            Log::ErrorFmt("ADD_HOOK: %s at %p failed: %s", #name, addr, error_msg);                \
 | 
			
		||||
        }                                                                                          \
 | 
			
		||||
        else {                                                                                     \
 | 
			
		||||
            hookedStubs.emplace(stub);                                                             \
 | 
			
		||||
            GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr);                         \
 | 
			
		||||
        }                                                                                          \
 | 
			
		||||
    }                                                                                              \
 | 
			
		||||
    else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr);                      \
 | 
			
		||||
    if (Config::lazyInit) UnityResolveProgress::classProgress.current++
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
void UnHookAll() {
 | 
			
		||||
    for (const auto i: hookedStubs) {
 | 
			
		||||
        int result = shadowhook_unhook(i);
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +40,7 @@ void UnHookAll() {
 | 
			
		|||
            GakumasLocal::Log::ErrorFmt("unhook failed: %d - %s", error_num, error_msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}*/
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal::HookMain {
 | 
			
		||||
    using Il2cppString = UnityResolve::UnityType::String;
 | 
			
		||||
| 
						 | 
				
			
			@ -300,9 +286,129 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
    struct TransparentStringHash : std::hash<std::wstring>, std::hash<std::wstring_view>
 | 
			
		||||
    {
 | 
			
		||||
        using is_transparent = void;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    typedef std::unordered_set<std::wstring, TransparentStringHash, std::equal_to<void>> AssetPathsType;
 | 
			
		||||
    std::map<std::string, AssetPathsType> CustomAssetBundleAssetPaths;
 | 
			
		||||
    std::unordered_map<std::string, uint32_t> CustomAssetBundleHandleMap{};
 | 
			
		||||
    std::list<std::string> g_extra_assetbundle_paths{};
 | 
			
		||||
 | 
			
		||||
    void LoadExtraAssetBundle() {
 | 
			
		||||
        using Il2CppString = UnityResolve::UnityType::String;
 | 
			
		||||
 | 
			
		||||
        if (g_extra_assetbundle_paths.empty()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // CustomAssetBundleHandleMap.clear();
 | 
			
		||||
        // CustomAssetBundleAssetPaths.clear();
 | 
			
		||||
        // assert(!ExtraAssetBundleHandle && ExtraAssetBundleAssetPaths.empty());
 | 
			
		||||
 | 
			
		||||
        static auto AssetBundle_GetAllAssetNames = reinterpret_cast<void* (*)(void*)>(
 | 
			
		||||
            Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundle::GetAllAssetNames()")
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        for (const auto& i : g_extra_assetbundle_paths) {
 | 
			
		||||
            if (CustomAssetBundleHandleMap.contains(i)) continue;
 | 
			
		||||
 | 
			
		||||
            const auto extraAssetBundle = WinHooks::LoadAssetBundle(i);
 | 
			
		||||
            if (extraAssetBundle)
 | 
			
		||||
            {
 | 
			
		||||
                const auto allAssetPaths = AssetBundle_GetAllAssetNames(extraAssetBundle);
 | 
			
		||||
                AssetPathsType assetPath{};
 | 
			
		||||
                Il2cppUtils::iterate_IEnumerable<Il2CppString*>(allAssetPaths, [&assetPath](Il2CppString* path)
 | 
			
		||||
                    {
 | 
			
		||||
                        // ExtraAssetBundleAssetPaths.emplace(path->start_char);
 | 
			
		||||
                        // printf("Asset loaded: %ls\n", path->start_char);
 | 
			
		||||
                        assetPath.emplace(path->start_char);
 | 
			
		||||
                    });
 | 
			
		||||
                CustomAssetBundleAssetPaths.emplace(i, assetPath);
 | 
			
		||||
                CustomAssetBundleHandleMap.emplace(i, UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", extraAssetBundle, false));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Log::ErrorFmt("Cannot load asset bundle: %s\n", i.c_str());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t GetBundleHandleByAssetName(std::wstring assetName) {
 | 
			
		||||
        for (const auto& i : CustomAssetBundleAssetPaths) {
 | 
			
		||||
            for (const auto& m : i.second) {
 | 
			
		||||
                if (std::equal(m.begin(), m.end(), assetName.begin(), assetName.end(),
 | 
			
		||||
                    [](wchar_t c1, wchar_t c2) {
 | 
			
		||||
                        return std::tolower(c1, std::locale()) == std::tolower(c2, std::locale());
 | 
			
		||||
                    })) {
 | 
			
		||||
                    return CustomAssetBundleHandleMap.at(i.first);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return NULL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t GetBundleHandleByAssetName(std::string assetName) {
 | 
			
		||||
        return GetBundleHandleByAssetName(utility::conversions::to_string_t(assetName));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t ReplaceFontHandle;
 | 
			
		||||
 | 
			
		||||
    void* GetReplaceFont() {
 | 
			
		||||
        static auto FontClass = Il2cppUtils::GetClass("UnityEngine.TextRenderingModule.dll", "UnityEngine", "Font");
 | 
			
		||||
        static auto Font_Type = UnityResolve::Invoke<Il2cppUtils::Il2CppReflectionType*>("il2cpp_type_get_object", 
 | 
			
		||||
            UnityResolve::Invoke<void*>("il2cpp_class_get_type", FontClass->address));
 | 
			
		||||
 | 
			
		||||
        using Il2CppString = UnityResolve::UnityType::String;
 | 
			
		||||
        const auto fontPath = "assets/fonts/gkamszhfontmix.otf";
 | 
			
		||||
 | 
			
		||||
        void* replaceFont{};
 | 
			
		||||
        const auto& bundleHandle = GetBundleHandleByAssetName(fontPath);
 | 
			
		||||
        if (bundleHandle)
 | 
			
		||||
        {
 | 
			
		||||
            if (ReplaceFontHandle)
 | 
			
		||||
            {
 | 
			
		||||
                replaceFont = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", ReplaceFontHandle);
 | 
			
		||||
                // 加载场景时会被 Resources.UnloadUnusedAssets 干掉,且不受 DontDestroyOnLoad 影响,暂且判断是否存活,并在必要的时候重新加载
 | 
			
		||||
                // TODO: 考虑挂载到 GameObject 上
 | 
			
		||||
                // AssetBundle 不会被干掉
 | 
			
		||||
                if (IsNativeObjectAlive(replaceFont))
 | 
			
		||||
                {
 | 
			
		||||
                    return replaceFont;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    UnityResolve::Invoke<void>("il2cpp_gchandle_free", std::exchange(ReplaceFontHandle, 0));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const auto extraAssetBundle = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", bundleHandle);
 | 
			
		||||
			static auto AssetBundle_LoadAsset = reinterpret_cast<void* (*)(void* _this, Il2CppString* name, Il2cppUtils::Il2CppReflectionType* type)>(
 | 
			
		||||
                Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")
 | 
			
		||||
				);;
 | 
			
		||||
 | 
			
		||||
            replaceFont = AssetBundle_LoadAsset(extraAssetBundle, Il2cppString::New(fontPath), Font_Type);
 | 
			
		||||
            if (replaceFont)
 | 
			
		||||
            {
 | 
			
		||||
                ReplaceFontHandle = UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", replaceFont, false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Log::Error("Cannot load asset font\n");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            Log::Error("Cannot find asset font\n");
 | 
			
		||||
        }
 | 
			
		||||
        return replaceFont;
 | 
			
		||||
    }
 | 
			
		||||
#else
 | 
			
		||||
    void* fontCache = nullptr;
 | 
			
		||||
    void* GetReplaceFont() {
 | 
			
		||||
        static std::string fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
 | 
			
		||||
        static auto fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
 | 
			
		||||
        if (!std::filesystem::exists(fontName)) {
 | 
			
		||||
            return nullptr;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -323,10 +429,11 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
        const auto newFont = Font_klass->New<void*>();
 | 
			
		||||
        Font_ctor->Invoke<void>(newFont);
 | 
			
		||||
 | 
			
		||||
        CreateFontFromPath(newFont, Il2cppString::New(fontName));
 | 
			
		||||
        CreateFontFromPath(newFont, Il2cppString::New(fontName.string()));
 | 
			
		||||
        fontCache = newFont;
 | 
			
		||||
        return newFont;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    std::unordered_set<void*> updatedFontPtrs{};
 | 
			
		||||
    void UpdateFont(void* TMP_Textself) {
 | 
			
		||||
| 
						 | 
				
			
			@ -350,6 +457,7 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
 | 
			
		||||
        auto newFont = GetReplaceFont();
 | 
			
		||||
        if (!newFont) return;
 | 
			
		||||
 | 
			
		||||
        auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
 | 
			
		||||
        if (fontAsset) {
 | 
			
		||||
            set_sourceFontFile->Invoke<void>(fontAsset, newFont);
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +467,9 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
            }
 | 
			
		||||
            if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear();
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
			Log::Error("UpdateFont: fontAsset is null.");
 | 
			
		||||
        }
 | 
			
		||||
        set_font->Invoke<void>(TMP_Textself, fontAsset);
 | 
			
		||||
 | 
			
		||||
//        auto fontMaterial = get_material->Invoke<void*>(fontAsset);
 | 
			
		||||
| 
						 | 
				
			
			@ -1264,8 +1375,17 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
 | 
			
		||||
    void StartInjectFunctions() {
 | 
			
		||||
        const auto hookInstaller = Plugin::GetInstance().GetHookInstaller();
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
        auto il2cpp_module = GetModuleHandle("GameAssembly.dll");
 | 
			
		||||
        if (!il2cpp_module) {
 | 
			
		||||
            Log::ErrorFmt("GameAssembly.dll not loaded.");
 | 
			
		||||
        }
 | 
			
		||||
        UnityResolve::Init(il2cpp_module, UnityResolve::Mode::Il2Cpp, Config::lazyInit);
 | 
			
		||||
#else
 | 
			
		||||
        UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW),
 | 
			
		||||
                           UnityResolve::Mode::Il2Cpp, Config::lazyInit);
 | 
			
		||||
            UnityResolve::Mode::Il2Cpp, Config::lazyInit);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
        ADD_HOOK(AssetBundle_LoadAssetAsync, Il2cppUtils::il2cpp_resolve_icall(
 | 
			
		||||
                "UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)"));
 | 
			
		||||
| 
						 | 
				
			
			@ -1471,11 +1591,21 @@ namespace GakumasLocal::HookMain {
 | 
			
		|||
                "UnityEngine.Application::set_targetFrameRate(System.Int32)"));
 | 
			
		||||
        ADD_HOOK(EndCameraRendering, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine.Rendering",
 | 
			
		||||
                                                                     "RenderPipeline", "EndCameraRendering"));
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
        g_extra_assetbundle_paths.push_back((gakumasLocalPath / "local-files/gakumasassets").string());
 | 
			
		||||
		LoadExtraAssetBundle();
 | 
			
		||||
#endif // GKMS_WINDOWS
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    // 77 2640 5000
 | 
			
		||||
 | 
			
		||||
    DEFINE_HOOK(int, il2cpp_init, (const char* domain_name)) {
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
        const auto ret = il2cpp_init_Orig(domain_name);
 | 
			
		||||
#else
 | 
			
		||||
        const auto ret = 0;
 | 
			
		||||
#endif
 | 
			
		||||
        // InjectFunctions();
 | 
			
		||||
 | 
			
		||||
        Log::Info("Waiting for config...");
 | 
			
		||||
| 
						 | 
				
			
			@ -1524,8 +1654,13 @@ namespace GakumasLocal::Hook {
 | 
			
		|||
 | 
			
		||||
        Log::Info("Installing hook");
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
        ADD_HOOK(HookMain::il2cpp_init,
 | 
			
		||||
                  Plugin::GetInstance().GetHookInstaller()->LookupSymbol("il2cpp_init"));
 | 
			
		||||
            Plugin::GetInstance().GetHookInstaller()->LookupSymbol("il2cpp_init"));
 | 
			
		||||
#else
 | 
			
		||||
        HookMain::il2cpp_init_Hook(nullptr);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Log::Info("Hook installed");
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -308,6 +308,22 @@ namespace Il2cppUtils {
 | 
			
		|||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    template <typename T = void*>
 | 
			
		||||
    static void iterate_IEnumerable(const void* obj, std::invocable<T> auto&& receiver)
 | 
			
		||||
    {
 | 
			
		||||
        const auto klass = get_class_from_instance(obj);
 | 
			
		||||
        const auto getEnumeratorMethod = reinterpret_cast<void* (*)(const void*)>(il2cpp_class_get_method_from_name(klass, "GetEnumerator", 0)->methodPointer);
 | 
			
		||||
        const auto enumerator = getEnumeratorMethod(obj);
 | 
			
		||||
        const auto enumeratorClass = get_class_from_instance(enumerator);
 | 
			
		||||
        const auto getCurrentMethod = reinterpret_cast<T(*)(void*)>(il2cpp_class_get_method_from_name(enumeratorClass, "get_Current", 0)->methodPointer);
 | 
			
		||||
        const auto moveNextMethod = reinterpret_cast<bool(*)(void*)>(il2cpp_class_get_method_from_name(enumeratorClass, "MoveNext", 0)->methodPointer);
 | 
			
		||||
 | 
			
		||||
        while (moveNextMethod(enumerator))
 | 
			
		||||
        {
 | 
			
		||||
            static_cast<decltype(receiver)>(receiver)(getCurrentMethod(enumerator));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    namespace Tools {
 | 
			
		||||
 | 
			
		||||
        template <typename T = void*>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ namespace GakumasLocal::Local {
 | 
			
		|||
            }
 | 
			
		||||
            std::ifstream file(filePath);
 | 
			
		||||
            if (!file.is_open()) {
 | 
			
		||||
                Log::ErrorFmt("Load %s failed.\n", filePath.c_str());
 | 
			
		||||
                Log::ErrorFmt("Load %s failed.\n", filePath.string().c_str());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            std::string fileContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +112,7 @@ namespace GakumasLocal::Local {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (std::exception& e) {
 | 
			
		||||
            Log::ErrorFmt("Load %s failed: %s\n", filePath.c_str(), e.what());
 | 
			
		||||
            Log::ErrorFmt("Load %s failed: %s\n", filePath.string().c_str(), e.what());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -447,7 +447,7 @@ namespace GakumasLocal::Local {
 | 
			
		|||
            const auto targetFilePath = basePath / "local-files" / "resource" / name;
 | 
			
		||||
            // Log::DebugFmt("GetResourceText: %s", targetFilePath.c_str());
 | 
			
		||||
            if (exists(targetFilePath)) {
 | 
			
		||||
                auto readStr = readFileToString(targetFilePath);
 | 
			
		||||
                auto readStr = readFileToString(targetFilePath.string());
 | 
			
		||||
                *ret = readStr;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,19 @@
 | 
			
		|||
#include "Log.h"
 | 
			
		||||
#include <android/log.h>
 | 
			
		||||
#include <Misc.hpp>
 | 
			
		||||
#include "Misc.hpp"
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <queue>
 | 
			
		||||
#include <cstdarg>
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    #include <android/log.h>
 | 
			
		||||
 | 
			
		||||
    extern JavaVM* g_javaVM;
 | 
			
		||||
    extern jclass g_gakumasHookMainClass;
 | 
			
		||||
    extern jmethodID showToastMethodId;
 | 
			
		||||
#endif // GKMS_WINDOWS
 | 
			
		||||
 | 
			
		||||
extern JavaVM* g_javaVM;
 | 
			
		||||
extern jclass g_gakumasHookMainClass;
 | 
			
		||||
extern jmethodID showToastMethodId;
 | 
			
		||||
 | 
			
		||||
#define GetParamStringResult(name)\
 | 
			
		||||
    va_list args;\
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +80,7 @@ namespace GakumasLocal::Log {
 | 
			
		|||
        __android_log_write(prio, "GakumasLog", result.c_str());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    void ShowToastJNI(const char* text) {
 | 
			
		||||
        DebugFmt("Toast: %s", text);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,15 +105,19 @@ namespace GakumasLocal::Log {
 | 
			
		|||
 | 
			
		||||
            g_javaVM->DetachCurrentThread();
 | 
			
		||||
        }).detach();
 | 
			
		||||
    }
 | 
			
		||||
    }*/
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    void ShowToast(const std::string& text) {
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
        showingToasts.push(text);
 | 
			
		||||
#else
 | 
			
		||||
		InfoFmt("Toast: %s", text.c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void ShowToast(const char* text) {
 | 
			
		||||
        DebugFmt("Toast: %s", text);
 | 
			
		||||
        // DebugFmt("Toast: %s", text);
 | 
			
		||||
        return ShowToast(std::string(text));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -125,6 +135,7 @@ namespace GakumasLocal::Log {
 | 
			
		|||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    void ToastLoop(JNIEnv *env, jclass clazz) {
 | 
			
		||||
        const auto toastString = GetQueuedToast();
 | 
			
		||||
        if (toastString.empty()) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -140,4 +151,6 @@ namespace GakumasLocal::Log {
 | 
			
		|||
            _showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,14 @@
 | 
			
		|||
#ifndef GAKUMAS_LOCALIFY_LOG_H
 | 
			
		||||
#define GAKUMAS_LOCALIFY_LOG_H
 | 
			
		||||
 | 
			
		||||
#include "../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    #include <jni.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal::Log {
 | 
			
		||||
    std::string StringFormat(const char* fmt, ...);
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +24,9 @@ namespace GakumasLocal::Log {
 | 
			
		|||
    void ShowToast(const char* text);
 | 
			
		||||
    void ShowToastFmt(const char* fmt, ...);
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    void ToastLoop(JNIEnv *env, jclass clazz);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif //GAKUMAS_LOCALIFY_LOG_H
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -541,7 +541,8 @@ namespace GakumasLocal::MasterLocal {
 | 
			
		|||
                    if (isFailed) break;
 | 
			
		||||
                }
 | 
			
		||||
                if (!isFailed) break;
 | 
			
		||||
                NextLoop:
 | 
			
		||||
            NextLoop:
 | 
			
		||||
                ;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (isFailed) return false;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,786 +0,0 @@
 | 
			
		|||
#include "MasterLocal.h"
 | 
			
		||||
#include "Local.h"
 | 
			
		||||
#include "Il2cppUtils.hpp"
 | 
			
		||||
#include "config/Config.hpp"
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <unordered_set>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <regex>
 | 
			
		||||
#include <nlohmann/json.hpp>
 | 
			
		||||
 | 
			
		||||
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<PKItem> pkItems;
 | 
			
		||||
        std::unordered_map<std::string, nlohmann::json> dataMap;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static std::unordered_map<std::string, TableInfo> g_loadedData;
 | 
			
		||||
    static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> fieldSetCache;
 | 
			
		||||
    static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> 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<char>(std::toupper(result[0]));
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Il2cppUtils::MethodInfo* GetGetSetMethodFromCache(const std::string& fieldName, int argsCount,
 | 
			
		||||
                                                          std::unordered_map<std::string, Il2cppUtils::MethodInfo*>& 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<typename T>
 | 
			
		||||
        T ReadField(const std::string& fieldName) {
 | 
			
		||||
            auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_");
 | 
			
		||||
            if (get_mtd) {
 | 
			
		||||
                return reinterpret_cast<T (*)(void*, void*)>(get_mtd->methodPointer)(self, get_mtd);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>(
 | 
			
		||||
                    "il2cpp_class_get_field_from_name",
 | 
			
		||||
                    self_klass,
 | 
			
		||||
                    (fieldName + '_').c_str()
 | 
			
		||||
            );
 | 
			
		||||
            if (!field) {
 | 
			
		||||
                return T();
 | 
			
		||||
            }
 | 
			
		||||
            return Il2cppUtils::ClassGetFieldValue<T>(self, field);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        template<typename T>
 | 
			
		||||
        void SetField(const std::string& fieldName, T value) {
 | 
			
		||||
            auto set_mtd = GetGetSetMethodFromCache(fieldName, 1, fieldSetCache, "set_");
 | 
			
		||||
            if (set_mtd) {
 | 
			
		||||
                reinterpret_cast<void (*)(void*, T, void*)>(
 | 
			
		||||
                        set_mtd->methodPointer
 | 
			
		||||
                )(self, value, set_mtd);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            auto field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>(
 | 
			
		||||
                    "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<int>(fieldName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Il2cppString* ReadStringField(const std::string& fieldName) {
 | 
			
		||||
            auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_");
 | 
			
		||||
            if (!get_mtd) {
 | 
			
		||||
                return ReadField<Il2cppString*>(fieldName);
 | 
			
		||||
            }
 | 
			
		||||
            auto returnClass = UnityResolve::Invoke<Il2cppUtils::Il2CppClassHead*>(
 | 
			
		||||
                    "il2cpp_class_from_type",
 | 
			
		||||
                    UnityResolve::Invoke<void*>("il2cpp_method_get_return_type", get_mtd)
 | 
			
		||||
            );
 | 
			
		||||
            if (!returnClass) {
 | 
			
		||||
                return reinterpret_cast<Il2cppString* (*)(void*, void*)>(
 | 
			
		||||
                        get_mtd->methodPointer
 | 
			
		||||
                )(self, get_mtd);
 | 
			
		||||
            }
 | 
			
		||||
            auto isEnum = UnityResolve::Invoke<bool>("il2cpp_class_is_enum", returnClass);
 | 
			
		||||
            if (!isEnum) {
 | 
			
		||||
                return reinterpret_cast<Il2cppString* (*)(void*, void*)>(
 | 
			
		||||
                        get_mtd->methodPointer
 | 
			
		||||
                )(self, get_mtd);
 | 
			
		||||
            }
 | 
			
		||||
            auto enumMap = Il2cppUtils::EnumToValueMap(returnClass, true);
 | 
			
		||||
            auto enumValue = reinterpret_cast<int (*)(void*, void*)>(
 | 
			
		||||
                    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<std::string>& 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<void (*)(void*, void*)>(
 | 
			
		||||
                    List_String_ctor_mtd->methodPointer
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            auto newList = UnityResolve::Invoke<void*>("il2cpp_object_new", List_String_klass);
 | 
			
		||||
            List_String_ctor(newList, List_String_ctor_mtd);
 | 
			
		||||
 | 
			
		||||
            Il2cppUtils::Tools::CSListEditor<Il2cppString*> newListEditor(newList);
 | 
			
		||||
            for (auto& s : data) {
 | 
			
		||||
                newListEditor.Add(Il2cppString::New(s));
 | 
			
		||||
            }
 | 
			
		||||
            SetField(fieldName, newList);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void* ReadObjectField(const std::string& fieldName) {
 | 
			
		||||
            return ReadField<void*>(fieldName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void* ReadObjectListField(const std::string& fieldName) {
 | 
			
		||||
            return ReadField<void*>(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<PKItem> parseAllPKItems(const nlohmann::json& row, const std::vector<std::string>& pkNames) {
 | 
			
		||||
        std::vector<PKItem> 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<std::string>();
 | 
			
		||||
        } else if (jval.is_number_integer()) {
 | 
			
		||||
            uniqueKey += std::to_string(jval.get<int>());
 | 
			
		||||
        }
 | 
			
		||||
        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<std::string>& 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<object> 类型
 | 
			
		||||
            // 这里仅检查 "顶层字段是否是空数组"
 | 
			
		||||
            // 若需要更深层的判断,需扩展
 | 
			
		||||
            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<std::string> buildSkipFields(const std::vector<PKItem>& pkItems) {
 | 
			
		||||
        std::unordered_set<std::string> 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<std::string>& skipSet,
 | 
			
		||||
            std::unordered_set<std::string>& 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<std::string>());
 | 
			
		||||
                    // Log::DebugFmt("localSet.insert: %s", val.get<std::string>().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<std::string>());
 | 
			
		||||
                    // Log::DebugFmt("localSet.insert: %s", element.get<std::string>().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<PKItem>& pkItems, std::unordered_set<std::string>& 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<std::string> pkNames;
 | 
			
		||||
                for (auto& x : j["rules"]["primaryKeys"]) {
 | 
			
		||||
                    pkNames.push_back(x.get<std::string>());
 | 
			
		||||
                }
 | 
			
		||||
                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<object> + 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<std::string>();
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        uniqueKey += std::to_string(subVal.get<int>());
 | 
			
		||||
                                    }
 | 
			
		||||
                                    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<object> + 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<void*> 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<std::string>& skipKeySet);
 | 
			
		||||
    void localizeArrayOfObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& arrVal, const std::unordered_set<std::string>& skipKeySet);
 | 
			
		||||
    void localizeObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& objVal, const std::unordered_set<std::string>& skipKeySet);
 | 
			
		||||
 | 
			
		||||
    //====================================================================
 | 
			
		||||
    // 对 array<object> 做一层递归 —— 需要带着 skipKeySet
 | 
			
		||||
    //====================================================================
 | 
			
		||||
    void localizeArrayOfObject(FieldController& fc, const std::string& fieldName, const nlohmann::json& arrVal, const std::unordered_set<std::string>& skipKeySet) {
 | 
			
		||||
        void* listPtr = fc.ReadObjectListField(fieldName);
 | 
			
		||||
        if (!listPtr) return;
 | 
			
		||||
        Il2cppUtils::Tools::CSListEditor<void*> listEdit(listPtr);
 | 
			
		||||
        int cmin = std::min<int>(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<std::string>& 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<std::string>& 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<std::string>();
 | 
			
		||||
                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<std::string>());
 | 
			
		||||
            }
 | 
			
		||||
            else if (val.is_array()) {
 | 
			
		||||
                if (!val.empty() && val.begin()->is_string()) {
 | 
			
		||||
                    bool allStr = true;
 | 
			
		||||
                    std::vector<std::string> strArray;
 | 
			
		||||
                    for (auto& x : val) {
 | 
			
		||||
                        if (!x.is_string()) { allStr = false; break; }
 | 
			
		||||
                        strArray.push_back(x.get<std::string>());
 | 
			
		||||
                    }
 | 
			
		||||
                    if (allStr) {
 | 
			
		||||
                        // Log::DebugFmt("SetStringListField in %s, key: %s", fc.self_klass->name, key.c_str());
 | 
			
		||||
                        fc.SetStringListField(key, strArray);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // array<object>
 | 
			
		||||
                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<std::string> 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<void*>* 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<void*>* 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<UnityResolve::UnityType::List<void*>*>(result));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void LocalizeMaster(void* result, const std::string& tableName) {
 | 
			
		||||
        if (!Config::useMasterTrans) return;
 | 
			
		||||
        LocalizeMasterTables(tableName, reinterpret_cast<UnityResolve::UnityType::List<void*>*>(result));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void LocalizeMasterItem(void* item, const std::string& tableName) {
 | 
			
		||||
        if (!Config::useMasterTrans) return;
 | 
			
		||||
        FieldController fc(item);
 | 
			
		||||
        LocalizeMasterItem(fc, tableName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
} // namespace GakumasLocal::MasterLocal
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
#ifndef GAKUMAS_LOCALIFY_MASTERLOCAL_H
 | 
			
		||||
#define GAKUMAS_LOCALIFY_MASTERLOCAL_H
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -2,11 +2,15 @@
 | 
			
		|||
 | 
			
		||||
#include <codecvt>
 | 
			
		||||
#include <locale>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include "fmt/core.h"
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    #include <jni.h>
 | 
			
		||||
    
 | 
			
		||||
extern JavaVM* g_javaVM;
 | 
			
		||||
    extern JavaVM* g_javaVM;
 | 
			
		||||
#else
 | 
			
		||||
    #include "cpprest/details/http_helpers.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal::Misc {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +24,13 @@ namespace GakumasLocal::Misc {
 | 
			
		|||
        return utf16conv.to_bytes(str.data(), str.data() + str.size());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
    std::string ToUTF8(const std::wstring_view& str) {
 | 
			
		||||
		return utility::conversions::to_utf8string(str.data());
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    JNIEnv* GetJNIEnv() {
 | 
			
		||||
        if (!g_javaVM) return nullptr;
 | 
			
		||||
        JNIEnv* env = nullptr;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +42,7 @@ namespace GakumasLocal::Misc {
 | 
			
		|||
        }
 | 
			
		||||
        return env;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    CSEnum::CSEnum(const std::string& name, const int value) {
 | 
			
		||||
        this->Add(name, value);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,16 @@
 | 
			
		|||
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <numeric>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    #include <jni.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal {
 | 
			
		||||
    using OpaqueFunctionPointer = void (*)();
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +19,13 @@ namespace GakumasLocal {
 | 
			
		|||
    namespace Misc {
 | 
			
		||||
        std::u16string ToUTF16(const std::string_view& str);
 | 
			
		||||
        std::string ToUTF8(const std::u16string_view& str);
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
        std::string ToUTF8(const std::wstring_view& str);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
        JNIEnv* GetJNIEnv();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
        class CSEnum {
 | 
			
		||||
        public:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,13 @@
 | 
			
		|||
#include "Misc.hpp"
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#ifndef GKMS_WINDOWS
 | 
			
		||||
    #include <jni.h>
 | 
			
		||||
#endif // !GKMS_WINDOWS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal {
 | 
			
		||||
    struct HookInstaller
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,12 @@
 | 
			
		|||
#include "baseCamera.hpp"
 | 
			
		||||
#include <thread>
 | 
			
		||||
 | 
			
		||||
#include "../../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
	#include <corecrt_math_defines.h>
 | 
			
		||||
#endif // GKMS_WINDOWS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace BaseCamera {
 | 
			
		||||
	using Vector3_t = UnityResolve::UnityType::Vector3;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "../deps/UnityResolve/UnityResolve.hpp"
 | 
			
		||||
#include "../../deps/UnityResolve/UnityResolve.hpp"
 | 
			
		||||
 | 
			
		||||
enum LonMoveHState {
 | 
			
		||||
	LonMoveLeftAndRight,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,16 @@
 | 
			
		|||
#include "baseCamera.hpp"
 | 
			
		||||
#include "camera.hpp"
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include "Misc.hpp"
 | 
			
		||||
#include "../Misc.hpp"
 | 
			
		||||
#include "../BaseDefine.h"
 | 
			
		||||
 | 
			
		||||
#include "../../platformDefine.hpp"
 | 
			
		||||
 | 
			
		||||
#ifdef GKMS_WINDOWS
 | 
			
		||||
    #include <corecrt_math_defines.h>
 | 
			
		||||
#endif // GKMS_WINDOWS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace GKCamera {
 | 
			
		||||
	BaseCamera::Camera baseCamera{};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
#include "baseCamera.hpp"
 | 
			
		||||
#include "Joystick/JoystickEvent.h"
 | 
			
		||||
#include "../../deps/Joystick/JoystickEvent.h"
 | 
			
		||||
 | 
			
		||||
namespace GKCamera {
 | 
			
		||||
    enum class CameraMode {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
#include <string>
 | 
			
		||||
#include "nlohmann/json.hpp"
 | 
			
		||||
#include "../Log.h"
 | 
			
		||||
#include <thread>
 | 
			
		||||
 | 
			
		||||
namespace GakumasLocal::Config {
 | 
			
		||||
    bool isConfigInit = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +101,6 @@ namespace GakumasLocal::Config {
 | 
			
		|||
            GetConfigItem(bLimitYy);
 | 
			
		||||
            GetConfigItem(bLimitZx);
 | 
			
		||||
            GetConfigItem(bLimitZy);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (std::exception& e) {
 | 
			
		||||
            Log::ErrorFmt("LoadConfig error: %s", e.what());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
#include "shadowhook.h"
 | 
			
		||||
#include <android/log.h>
 | 
			
		||||
 | 
			
		||||
#define ADD_HOOK(name, addr)                                                                       \
 | 
			
		||||
	name##_Addr = reinterpret_cast<name##_Type>(addr);                                             \
 | 
			
		||||
	if (addr) {                                                                                    \
 | 
			
		||||
    	auto stub = hookInstaller->InstallHook(reinterpret_cast<void*>(addr),                      \
 | 
			
		||||
                                               reinterpret_cast<void*>(name##_Hook),               \
 | 
			
		||||
                                               reinterpret_cast<void**>(&name##_Orig));            \
 | 
			
		||||
        if (stub == NULL) {                                                                        \
 | 
			
		||||
            int error_num = shadowhook_get_errno();                                                \
 | 
			
		||||
            const char *error_msg = shadowhook_to_errmsg(error_num);                               \
 | 
			
		||||
            Log::ErrorFmt("ADD_HOOK: %s at %p failed: %s", #name, addr, error_msg);                \
 | 
			
		||||
        }                                                                                          \
 | 
			
		||||
        else {                                                                                     \
 | 
			
		||||
            hookedStubs.emplace(stub);                                                             \
 | 
			
		||||
            GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr);                         \
 | 
			
		||||
        }                                                                                          \
 | 
			
		||||
    }                                                                                              \
 | 
			
		||||
    else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr);                      \
 | 
			
		||||
    if (Config::lazyInit) UnityResolveProgress::classProgress.current++
 | 
			
		||||
| 
						 | 
				
			
			@ -314,8 +314,8 @@ fun HomePage(modifier: Modifier = Modifier,
 | 
			
		|||
                                                fontSize = 14f,
 | 
			
		||||
                                                value = programConfig.value.useAPIAssetsURL,
 | 
			
		||||
                                                onValueChange = { c -> context?.onPUseAPIAssetsURLChanged(c, 0, 0, 0)},
 | 
			
		||||
                                                label = { Text(stringResource(R.string.api_addr)) },
 | 
			
		||||
                                                keyboardOptions = keyboardOptionsNumber)
 | 
			
		||||
                                                label = { Text(stringResource(R.string.api_addr)) }
 | 
			
		||||
                                            )
 | 
			
		||||
 | 
			
		||||
                                            if (downloadAble) {
 | 
			
		||||
                                                GakuButton(modifier = modifier
 | 
			
		||||
| 
						 | 
				
			
			@ -411,8 +411,8 @@ fun HomePage(modifier: Modifier = Modifier,
 | 
			
		|||
                                                fontSize = 14f,
 | 
			
		||||
                                                value = programConfig.value.transRemoteZipUrl,
 | 
			
		||||
                                                onValueChange = { c -> context?.onPTransRemoteZipUrlChanged(c, 0, 0, 0)},
 | 
			
		||||
                                                label = { Text(stringResource(id = R.string.resource_url)) },
 | 
			
		||||
                                                keyboardOptions = keyboardOptionsNumber)
 | 
			
		||||
                                                label = { Text(stringResource(id = R.string.resource_url)) }
 | 
			
		||||
                                            )
 | 
			
		||||
 | 
			
		||||
                                            if (downloadAble) {
 | 
			
		||||
                                                GakuButton(modifier = modifier
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue