Compare commits
3 Commits
c50fdfd678
...
01591e61c0
Author | SHA1 | Date |
---|---|---|
|
01591e61c0 | |
|
56c066bf42 | |
|
35c2b9f489 |
|
@ -15,8 +15,8 @@ android {
|
||||||
applicationId "io.github.chinosk.gakumas.localify"
|
applicationId "io.github.chinosk.gakumas.localify"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 5
|
versionCode 10
|
||||||
versionName "v1.6.8"
|
versionName "v2.0.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
|
@ -42,6 +42,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
GakumasLocalify/Log.cpp
|
GakumasLocalify/Log.cpp
|
||||||
GakumasLocalify/Misc.cpp
|
GakumasLocalify/Misc.cpp
|
||||||
GakumasLocalify/Local.cpp
|
GakumasLocalify/Local.cpp
|
||||||
|
GakumasLocalify/MasterLocal.cpp
|
||||||
GakumasLocalify/camera/baseCamera.cpp
|
GakumasLocalify/camera/baseCamera.cpp
|
||||||
GakumasLocalify/camera/camera.cpp
|
GakumasLocalify/camera/camera.cpp
|
||||||
GakumasLocalify/config/Config.cpp
|
GakumasLocalify/config/Config.cpp
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "../deps/UnityResolve/UnityResolve.hpp"
|
#include "../deps/UnityResolve/UnityResolve.hpp"
|
||||||
#include "Il2cppUtils.hpp"
|
#include "Il2cppUtils.hpp"
|
||||||
#include "Local.h"
|
#include "Local.h"
|
||||||
|
#include "MasterLocal.h"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "camera/camera.hpp"
|
#include "camera/camera.hpp"
|
||||||
#include "config/Config.hpp"
|
#include "config/Config.hpp"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
|
||||||
std::unordered_set<void*> hookedStubs{};
|
std::unordered_set<void*> hookedStubs{};
|
||||||
|
@ -421,6 +423,66 @@ namespace GakumasLocal::HookMain {
|
||||||
TextField_set_value_Orig(self, value);
|
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<UnityResolve::UnityType::Byte>* getAllSQL,
|
||||||
|
int sqlLength, UnityResolve::UnityType::List<void*>* result, void* predicate, void* comparison, void* mtd)) {
|
||||||
|
// result: List<Campus.Common.Proto.Client.Master.*>, 和 query 的表名一致
|
||||||
|
|
||||||
|
MasterBase_GetAll_Orig(self, getAllSQL, sqlLength, result, predicate, comparison, mtd);
|
||||||
|
|
||||||
|
auto data_ptr = reinterpret_cast<std::uint8_t*>(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<Il2cppString* (*)(void*, void*)>(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)) {
|
DEFINE_HOOK(Il2cppString*, OctoCaching_GetResourceFileName, (void* data, void* method)) {
|
||||||
auto ret = OctoCaching_GetResourceFileName_Orig(data, method);
|
auto ret = OctoCaching_GetResourceFileName_Orig(data, method);
|
||||||
//Log::DebugFmt("OctoCaching_GetResourceFileName: %s", ret->ToString().c_str());
|
//Log::DebugFmt("OctoCaching_GetResourceFileName: %s", ret->ToString().c_str());
|
||||||
|
@ -1093,6 +1155,68 @@ namespace GakumasLocal::HookMain {
|
||||||
return CampusActorAnimation_Setup_Orig(self, rootTrans, initializeData);
|
return CampusActorAnimation_Setup_Orig(self, rootTrans, initializeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
std::map<std::string, std::pair<uintptr_t, void*>> 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<decltype(FindByKeyHooks)*>(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<decltype(FindByKeyHooks)*>(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<void(*)(HookInstaller* hookInstaller)> 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<decltype(name##_FindByKey_Hook)*>( \
|
||||||
|
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() {
|
void StartInjectFunctions() {
|
||||||
const auto hookInstaller = Plugin::GetInstance().GetHookInstaller();
|
const auto hookInstaller = Plugin::GetInstance().GetHookInstaller();
|
||||||
UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW),
|
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",
|
ADD_HOOK(TextField_set_value, Il2cppUtils::GetMethodPointer("UnityEngine.UIElementsModule.dll", "UnityEngine.UIElements",
|
||||||
"TextField", "set_value"));
|
"TextField", "set_value"));
|
||||||
|
/* SQL 查询相关函数,不好用
|
||||||
|
// 下面是 byte[] u8 string 转 std::string 的例子
|
||||||
|
auto query = reinterpret_cast<UnityResolve::UnityType::Array<UnityResolve::UnityType::Byte>*>(mtd);
|
||||||
|
auto data_ptr = reinterpret_cast<std::uint8_t*>(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<System.Byte>"}));
|
||||||
|
|
||||||
|
/* // 此 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",
|
ADD_HOOK(OctoCaching_GetResourceFileName, Il2cppUtils::GetMethodPointer("Octo.dll", "Octo.Caching",
|
||||||
"OctoCaching", "GetResourceFileName"));
|
"OctoCaching", "GetResourceFileName"));
|
||||||
|
@ -1304,11 +1461,13 @@ namespace GakumasLocal::HookMain {
|
||||||
}
|
}
|
||||||
|
|
||||||
Local::LoadData();
|
Local::LoadData();
|
||||||
|
MasterLocal::LoadData();
|
||||||
|
|
||||||
if (Config::lazyInit) {
|
if (Config::lazyInit) {
|
||||||
UnityResolveProgress::classProgress.current = 1;
|
UnityResolveProgress::classProgress.current = 1;
|
||||||
UnityResolveProgress::startInit = false;
|
// UnityResolveProgress::startInit = false;
|
||||||
}
|
}
|
||||||
|
UnityResolveProgress::startInit = false;
|
||||||
|
|
||||||
Log::Info("Plugin init finished.");
|
Log::Info("Plugin init finished.");
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
@ -14,28 +14,6 @@ namespace Il2cppUtils {
|
||||||
const char* namespaze;
|
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
|
struct Il2CppObject
|
||||||
{
|
{
|
||||||
union
|
union
|
||||||
|
@ -110,7 +88,37 @@ namespace Il2cppUtils {
|
||||||
int herz;
|
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 std::string& className) {
|
||||||
const auto assembly = UnityResolve::Get(assemblyName);
|
const auto assembly = UnityResolve::Get(assemblyName);
|
||||||
if (!assembly) {
|
if (!assembly) {
|
||||||
|
@ -149,7 +157,7 @@ namespace Il2cppUtils {
|
||||||
return ret;
|
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<std::string>& args = {}) {
|
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
|
||||||
const auto assembly = UnityResolve::Get(assemblyName);
|
const auto assembly = UnityResolve::Get(assemblyName);
|
||||||
if (!assembly) {
|
if (!assembly) {
|
||||||
|
@ -176,7 +184,7 @@ namespace Il2cppUtils {
|
||||||
return method;
|
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<std::string>& args = {}) {
|
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
|
||||||
auto method = GetMethod(assemblyName, nameSpaceName, className, methodName, args);
|
auto method = GetMethod(assemblyName, nameSpaceName, className, methodName, args);
|
||||||
if (method) {
|
if (method) {
|
||||||
|
@ -185,20 +193,19 @@ namespace Il2cppUtils {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* il2cpp_resolve_icall(const char* s) {
|
static void* il2cpp_resolve_icall(const char* s) {
|
||||||
return UnityResolve::Invoke<void*>("il2cpp_resolve_icall", s);
|
return UnityResolve::Invoke<void*>("il2cpp_resolve_icall", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
Il2CppClassHead* get_class_from_instance(const void* instance) {
|
static Il2CppClassHead* get_class_from_instance(const void* instance) {
|
||||||
return static_cast<Il2CppClassHead*>(*static_cast<void* const*>(std::assume_aligned<alignof(void*)>(instance)));
|
return static_cast<Il2CppClassHead*>(*static_cast<void* const*>(std::assume_aligned<alignof(void*)>(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<MethodInfo*>("il2cpp_class_get_method_from_name", klass, name, argsCount);
|
return UnityResolve::Invoke<MethodInfo*>("il2cpp_class_get_method_from_name", klass, name, argsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void* find_nested_class(void* klass, std::predicate<void*> auto&& predicate)
|
static void* find_nested_class(void* klass, std::predicate<void*> auto&& predicate) {
|
||||||
{
|
|
||||||
void* iter{};
|
void* iter{};
|
||||||
while (const auto curNestedClass = UnityResolve::Invoke<void*>("il2cpp_class_get_nested_types", klass, &iter))
|
while (const auto curNestedClass = UnityResolve::Invoke<void*>("il2cpp_class_get_nested_types", klass, &iter))
|
||||||
{
|
{
|
||||||
|
@ -211,24 +218,34 @@ namespace Il2cppUtils {
|
||||||
return nullptr;
|
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 find_nested_class(klass, [name = std::string_view(name)](void* nestedClass) {
|
||||||
return static_cast<Il2CppClassHead*>(nestedClass)->name == name;
|
return static_cast<Il2CppClassHead*>(nestedClass)->name == name;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename RType>
|
template <typename RType>
|
||||||
auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType {
|
static auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType {
|
||||||
return *reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset);
|
return *reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename RType>
|
template <typename RType>
|
||||||
auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, RType value) -> void {
|
static auto ClassGetFieldValue(void* obj, FieldInfo* field) -> RType {
|
||||||
|
return *reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, T value) -> void {
|
||||||
|
const auto fieldPtr = static_cast<std::byte*>(obj) + field->offset;
|
||||||
|
std::memcpy(fieldPtr, std::addressof(value), sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename RType>
|
||||||
|
static auto ClassSetFieldValue(void* obj, FieldInfo* field, RType value) -> void {
|
||||||
*reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset) = value;
|
*reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(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;
|
using Il2CppString = UnityResolve::UnityType::String;
|
||||||
|
|
||||||
static auto assemblyLoad = reinterpret_cast<void* (*)(Il2CppString*)>(
|
static auto assemblyLoad = reinterpret_cast<void* (*)(Il2CppString*)>(
|
||||||
|
@ -245,6 +262,43 @@ namespace Il2cppUtils {
|
||||||
return UnityResolve::Invoke<void*>("il2cpp_class_from_system_type", reflectionType);
|
return UnityResolve::Invoke<void*>("il2cpp_class_from_system_type", reflectionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::unordered_map<std::string, std::unordered_map<int, std::string>> enumToValueMapCache{};
|
||||||
|
static std::unordered_map<int, std::string> EnumToValueMap(Il2CppClassHead* enumClass, bool useCache) {
|
||||||
|
std::unordered_map<int, std::string> ret{};
|
||||||
|
auto isEnum = UnityResolve::Invoke<bool>("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<Il2cppUtils::FieldInfo*>("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<void>("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 {
|
namespace Tools {
|
||||||
|
|
||||||
template <typename T = void*>
|
template <typename T = void*>
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace GakumasLocal::Local {
|
namespace GakumasLocal::Local {
|
||||||
|
extern std::unordered_set<std::string> translatedText;
|
||||||
|
|
||||||
std::filesystem::path GetBasePath();
|
std::filesystem::path GetBasePath();
|
||||||
void LoadData();
|
void LoadData();
|
||||||
bool GetI18n(const std::string& key, std::string* ret);
|
bool GetI18n(const std::string& key, std::string* ret);
|
||||||
|
|
|
@ -0,0 +1,807 @@
|
||||||
|
#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;
|
||||||
|
|
||||||
|
static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> fieldSetCache;
|
||||||
|
static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> fieldGetCache;
|
||||||
|
|
||||||
|
enum class JsonValueType {
|
||||||
|
JVT_String,
|
||||||
|
JVT_Int,
|
||||||
|
JVT_Object,
|
||||||
|
JVT_ArrayObject,
|
||||||
|
JVT_ArrayString,
|
||||||
|
JVT_Unsupported,
|
||||||
|
JVT_NeedMore_EmptyArray
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ItemRule {
|
||||||
|
std::vector<std::string> mainPrimaryKey;
|
||||||
|
std::map<std::string, std::vector<std::string>> subPrimaryKey;
|
||||||
|
|
||||||
|
std::vector<std::string> mainLocalKey;
|
||||||
|
std::map<std::string, std::vector<std::string>> subLocalKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TableLocalData {
|
||||||
|
ItemRule itemRule;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, JsonValueType> mainKeyType;
|
||||||
|
std::unordered_map<std::string, std::unordered_map<std::string, JsonValueType>> subKeyType;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> transData;
|
||||||
|
std::unordered_map<std::string, std::vector<std::string>> 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<std::string, TableLocalData> 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<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) {
|
||||||
|
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<typename T>
|
||||||
|
T ReadField(const std::string& fieldName) {
|
||||||
|
if (!self) return T();
|
||||||
|
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) {
|
||||||
|
if (!self) return;
|
||||||
|
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) {
|
||||||
|
if (!self) return nullptr;
|
||||||
|
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) {
|
||||||
|
if (!self) return;
|
||||||
|
auto newString = Il2cppString::New(value);
|
||||||
|
SetField(fieldName, newString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetStringListField(const std::string& fieldName, const std::vector<std::string>& 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<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) {
|
||||||
|
if (!self) return nullptr;
|
||||||
|
return ReadField<void*>(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void* ReadObjectListField(const std::string& fieldName) {
|
||||||
|
if (!self) return nullptr;
|
||||||
|
return ReadField<void*>(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<std::string> 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<std::string>{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<int>()));
|
||||||
|
}
|
||||||
|
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<std::string>()); // 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<int>()));
|
||||||
|
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<std::string> 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<void*> 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
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef GAKUMAS_LOCALIFY_MASTERLOCAL_H
|
||||||
|
#define GAKUMAS_LOCALIFY_MASTERLOCAL_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace GakumasLocal::MasterLocal {
|
||||||
|
void LoadData();
|
||||||
|
|
||||||
|
void LocalizeMasterItem(void* item, const std::string& tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //GAKUMAS_LOCALIFY_MASTERLOCAL_H
|
|
@ -0,0 +1,786 @@
|
||||||
|
#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
|
|
@ -0,0 +1,15 @@
|
||||||
|
#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
|
|
@ -168,6 +168,33 @@ namespace GakumasLocal::Misc {
|
||||||
return fmt;
|
return fmt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split(const std::string& str, char delimiter) {
|
||||||
|
std::vector<std::string> 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<std::string, std::string> 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, ""};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,8 @@ namespace GakumasLocal {
|
||||||
|
|
||||||
namespace StringFormat {
|
namespace StringFormat {
|
||||||
std::string stringFormatString(const std::string& fmt, const std::vector<std::string>& vec);
|
std::string stringFormatString(const std::string& fmt, const std::vector<std::string>& vec);
|
||||||
|
std::vector<std::string> split(const std::string& str, char delimiter);
|
||||||
|
std::pair<std::string, std::string> split_once(const std::string& str, const std::string& delimiter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ namespace GakumasLocal::Config {
|
||||||
bool replaceFont = true;
|
bool replaceFont = true;
|
||||||
bool forceExportResource = true;
|
bool forceExportResource = true;
|
||||||
bool textTest = false;
|
bool textTest = false;
|
||||||
|
bool useMasterTrans = true;
|
||||||
int gameOrientation = 0;
|
int gameOrientation = 0;
|
||||||
bool dumpText = false;
|
bool dumpText = false;
|
||||||
bool enableFreeCamera = false;
|
bool enableFreeCamera = false;
|
||||||
|
@ -64,6 +65,7 @@ namespace GakumasLocal::Config {
|
||||||
GetConfigItem(forceExportResource);
|
GetConfigItem(forceExportResource);
|
||||||
GetConfigItem(gameOrientation);
|
GetConfigItem(gameOrientation);
|
||||||
GetConfigItem(textTest);
|
GetConfigItem(textTest);
|
||||||
|
GetConfigItem(useMasterTrans);
|
||||||
GetConfigItem(dumpText);
|
GetConfigItem(dumpText);
|
||||||
GetConfigItem(targetFrameRate);
|
GetConfigItem(targetFrameRate);
|
||||||
GetConfigItem(enableFreeCamera);
|
GetConfigItem(enableFreeCamera);
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace GakumasLocal::Config {
|
||||||
extern bool forceExportResource;
|
extern bool forceExportResource;
|
||||||
extern int gameOrientation;
|
extern int gameOrientation;
|
||||||
extern bool textTest;
|
extern bool textTest;
|
||||||
|
extern bool useMasterTrans;
|
||||||
extern bool dumpText;
|
extern bool dumpText;
|
||||||
extern bool enableFreeCamera;
|
extern bool enableFreeCamera;
|
||||||
extern int targetFrameRate;
|
extern int targetFrameRate;
|
||||||
|
|
|
@ -309,7 +309,7 @@ public:
|
||||||
pDomain = Invoke<void*>("il2cpp_domain_get");
|
pDomain = Invoke<void*>("il2cpp_domain_get");
|
||||||
Invoke<void*>("il2cpp_thread_attach", pDomain);
|
Invoke<void*>("il2cpp_thread_attach", pDomain);
|
||||||
ForeachAssembly();
|
ForeachAssembly();
|
||||||
if (!lazyInit) UnityResolveProgress::startInit = false;
|
// if (!lazyInit) UnityResolveProgress::startInit = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pDomain = Invoke<void*>("mono_get_root_domain");
|
pDomain = Invoke<void*>("mono_get_root_domain");
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface ConfigListener {
|
||||||
fun onForceExportResourceChanged(value: Boolean)
|
fun onForceExportResourceChanged(value: Boolean)
|
||||||
fun onLoginAsIOSChanged(value: Boolean)
|
fun onLoginAsIOSChanged(value: Boolean)
|
||||||
fun onTextTestChanged(value: Boolean)
|
fun onTextTestChanged(value: Boolean)
|
||||||
|
fun onUseMasterTransChanged(value: Boolean)
|
||||||
fun onReplaceFontChanged(value: Boolean)
|
fun onReplaceFontChanged(value: Boolean)
|
||||||
fun onLazyInitChanged(value: Boolean)
|
fun onLazyInitChanged(value: Boolean)
|
||||||
fun onEnableFreeCameraChanged(value: Boolean)
|
fun onEnableFreeCameraChanged(value: Boolean)
|
||||||
|
@ -138,6 +139,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
||||||
saveConfig()
|
saveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUseMasterTransChanged(value: Boolean) {
|
||||||
|
config.useMasterTrans = value
|
||||||
|
saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDumpTextChanged(value: Boolean) {
|
override fun onDumpTextChanged(value: Boolean) {
|
||||||
config.dumpText = value
|
config.dumpText = value
|
||||||
saveConfig()
|
saveConfig()
|
||||||
|
|
|
@ -146,6 +146,7 @@ object FilesChecker {
|
||||||
val genericTransDir = File(localFilesDir, "genericTrans")
|
val genericTransDir = File(localFilesDir, "genericTrans")
|
||||||
val genericTransFile = File(localFilesDir, "generic.json")
|
val genericTransFile = File(localFilesDir, "generic.json")
|
||||||
val i18nFile = File(localFilesDir, "localization.json")
|
val i18nFile = File(localFilesDir, "localization.json")
|
||||||
|
val masterTransDir = File(localFilesDir, "masterTrans")
|
||||||
|
|
||||||
if (fontFile.exists()) {
|
if (fontFile.exists()) {
|
||||||
fontFile.delete()
|
fontFile.delete()
|
||||||
|
@ -156,6 +157,9 @@ object FilesChecker {
|
||||||
if (deleteRecursively(genericTransDir)) {
|
if (deleteRecursively(genericTransDir)) {
|
||||||
genericTransDir.mkdirs()
|
genericTransDir.mkdirs()
|
||||||
}
|
}
|
||||||
|
if (deleteRecursively(masterTransDir)) {
|
||||||
|
masterTransDir.mkdirs()
|
||||||
|
}
|
||||||
if (genericTransFile.exists()) {
|
if (genericTransFile.exists()) {
|
||||||
genericTransFile.writeText("{}")
|
genericTransFile.writeText("{}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ data class GakumasConfig (
|
||||||
var lazyInit: Boolean = true,
|
var lazyInit: Boolean = true,
|
||||||
var replaceFont: Boolean = true,
|
var replaceFont: Boolean = true,
|
||||||
var textTest: Boolean = false,
|
var textTest: Boolean = false,
|
||||||
|
var useMasterTrans: Boolean = true,
|
||||||
var dumpText: Boolean = false,
|
var dumpText: Boolean = false,
|
||||||
var gameOrientation: Int = 0,
|
var gameOrientation: Int = 0,
|
||||||
var forceExportResource: Boolean = false,
|
var forceExportResource: Boolean = false,
|
||||||
|
|
|
@ -75,6 +75,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
|
||||||
item {
|
item {
|
||||||
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
|
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
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) {
|
GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) {
|
||||||
v -> context?.onTextTestChanged(v)
|
v -> context?.onTextTestChanged(v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,7 @@
|
||||||
<string name="template_percent">%1$d パーセント。</string>
|
<string name="template_percent">%1$d パーセント。</string>
|
||||||
<string name="test_mode_live">テストモード - ライブ</string>
|
<string name="test_mode_live">テストモード - ライブ</string>
|
||||||
<string name="text_hook_test_mode">テキストフックテストモード</string>
|
<string name="text_hook_test_mode">テキストフックテストモード</string>
|
||||||
|
<string name="useMasterDBTrans">MasterDB をローカライズする</string>
|
||||||
<string name="translation_repository">翻訳のリポジトリ</string>
|
<string name="translation_repository">翻訳のリポジトリ</string>
|
||||||
<string name="translation_resource_update">翻訳リソースをアップデート</string>
|
<string name="translation_resource_update">翻訳リソースをアップデート</string>
|
||||||
<string name="unlockAllLive">すべてのライブを開放</string>
|
<string name="unlockAllLive">すべてのライブを開放</string>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<string name="useCustomeGraphicSettings">使用自定义画质设置</string>
|
<string name="useCustomeGraphicSettings">使用自定义画质设置</string>
|
||||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||||
<string name="text_hook_test_mode">文本 hook 测试模式</string>
|
<string name="text_hook_test_mode">文本 hook 测试模式</string>
|
||||||
|
<string name="useMasterDBTrans">使用 MasterDB 本地化</string>
|
||||||
<string name="export_text">导出文本</string>
|
<string name="export_text">导出文本</string>
|
||||||
<string name="force_export_resource">启动后强制导出资源</string>
|
<string name="force_export_resource">启动后强制导出资源</string>
|
||||||
<string name="login_as_ios">以 iOS 登陆</string>
|
<string name="login_as_ios">以 iOS 登陆</string>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<string name="useCustomeGraphicSettings">Use Custom Graphics Settings</string>
|
<string name="useCustomeGraphicSettings">Use Custom Graphics Settings</string>
|
||||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||||
<string name="text_hook_test_mode">Text Hook Test Mode</string>
|
<string name="text_hook_test_mode">Text Hook Test Mode</string>
|
||||||
|
<string name="useMasterDBTrans">Enable MasterDB Localization</string>
|
||||||
<string name="export_text">Export Text</string>
|
<string name="export_text">Export Text</string>
|
||||||
<string name="force_export_resource">Force Update Resource</string>
|
<string name="force_export_resource">Force Update Resource</string>
|
||||||
<string name="login_as_ios">Login as iOS</string>
|
<string name="login_as_ios">Login as iOS</string>
|
||||||
|
|
|
@ -17,7 +17,7 @@ lifecycle = "2.8.2"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
navigationCompose = "2.7.7"
|
navigationCompose = "2.7.7"
|
||||||
xdl = "2.1.1"
|
xdl = "2.1.1"
|
||||||
shadowhook = "1.0.9"
|
shadowhook = "1.0.10"
|
||||||
serialization="1.7.1"
|
serialization="1.7.1"
|
||||||
zip4j = "2.9.1"
|
zip4j = "2.9.1"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue