20 Commits

Author SHA1 Message Date
chinosk
60c846b2f0 update README 2025-01-09 07:28:00 +00:00
chinosk
01591e61c0 Merge pull request #7 from imas-tools/feature-masterdb-localization
MasterDB Localization
2025-01-09 01:21:08 -06:00
chinosk
56c066bf42 bump version to v2.0.1 2025-01-09 06:54:23 +00:00
chinosk
35c2b9f489 MasterDB Localization 2025-01-05 22:36:12 +00:00
chinosk
c50fdfd678 bump version to 1.6.8 2024-12-24 02:50:27 +00:00
chinosk
3c1d1f139a fix game crash 2024-12-24 02:49:30 +00:00
chinosk
6ac94178fa add ListEditor tool function 2024-12-24 02:33:56 +00:00
chinosk
c27085772f bump version to 1.6.7 2024-12-24 00:52:55 +00:00
chinosk
361c48e2c9 fix: incomplete hooks 2024-12-24 00:52:11 +00:00
chinosk
e9ba8b58fd bump version 2024-12-23 00:49:04 +00:00
chinosk
e03736bd7d fix crash when using remote files 2024-12-23 00:44:10 +00:00
chinosk
06b552a097 Built-in patcher supports Android 15
Source: JingMatrix/LSPatch
2024-12-01 04:51:53 +00:00
chinosk
bd9bcae01d Add login as ios.
Trim version string.
2024-12-01 03:49:58 +00:00
chinosk
8c850ad7db update submodule 2024-11-23 15:19:41 +00:00
chinosk
7bf429336b fix build error, add card name suffixes match 2024-11-22 22:45:52 +00:00
chinosk
c7e3d4f718 Add Japanese UI strings by @reindex-ot
Co-authored-by: Re*Index. (ot_inc) <32851879+reindex-ot@users.noreply.github.com>
2024-09-09 16:49:33 +08:00
chinosk
b74713be78 update version 2024-09-05 20:04:14 +08:00
chinosk
67945c86dd update submodule 2024-09-05 19:28:06 +08:00
chinosk
06a96a450e update README 2024-09-05 19:15:00 +08:00
chinosk
6e512d9380 Fix game crash (#6) 2024-09-05 19:09:47 +08:00
35 changed files with 2612 additions and 52 deletions

View File

@@ -8,16 +8,7 @@
# Usage
- 这是一个 XPosed 插件,已 Root 用户可以使用 [LSPosed](https://github.com/LSPosed/LSPosed),未 Root 用户可以使用 [LSPatch](https://github.com/LSPosed/LSPatch)。
# TODO
- [x] 卡片信息、TIPS 等部分的文本 hook (`generic`)
- [ ] 更多类型的文件替换
- [ ] LSPatch 集成模式无效
... and more
- 安卓 15 及以上的用户,请使用 [JingMatrix/LSPosed](https://github.com/JingMatrix/LSPosed) 或 [JingMatrix/LSPatch](https://github.com/JingMatrix/LSPatch)。因为原版已停止更新。

View File

@@ -15,8 +15,8 @@ android {
applicationId "io.github.chinosk.gakumas.localify"
minSdk 29
targetSdk 34
versionCode 4
versionName "v1.6.1"
versionCode 10
versionName "v2.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

Binary file not shown.

Binary file not shown.

3
app/lint.xml Normal file
View File

@@ -0,0 +1,3 @@
<lint>
<issue id="ExtraTranslation" severity="ignore" />
</lint>

View File

@@ -42,6 +42,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
GakumasLocalify/Log.cpp
GakumasLocalify/Misc.cpp
GakumasLocalify/Local.cpp
GakumasLocalify/MasterLocal.cpp
GakumasLocalify/camera/baseCamera.cpp
GakumasLocalify/camera/camera.cpp
GakumasLocalify/config/Config.cpp

View File

@@ -5,6 +5,7 @@
#include "../deps/UnityResolve/UnityResolve.hpp"
#include "Il2cppUtils.hpp"
#include "Local.h"
#include "MasterLocal.h"
#include <unordered_set>
#include "camera/camera.hpp"
#include "config/Config.hpp"
@@ -12,6 +13,7 @@
#include <jni.h>
#include <thread>
#include <map>
#include <set>
std::unordered_set<void*> hookedStubs{};
@@ -421,6 +423,66 @@ namespace GakumasLocal::HookMain {
TextField_set_value_Orig(self, value);
}
// 未使用的 Hook
DEFINE_HOOK(void, EffectGroup_ctor, (void* self, void* mtd)) {
// auto self_klass = Il2cppUtils::get_class_from_instance(self);
// Log::DebugFmt("EffectGroup_ctor: self: %s::%s", self_klass->namespaze, self_klass->name);
EffectGroup_ctor_Orig(self, mtd);
}
// 用于本地化 MasterDB
DEFINE_HOOK(void, MessageExtensions_MergeFrom, (void* message, void* span, void* mtd)) {
MessageExtensions_MergeFrom_Orig(message, span, mtd);
if (message) {
auto ret_klass = Il2cppUtils::get_class_from_instance(message);
if (ret_klass) {
// Log::DebugFmt("LocalizeMasterItem: %s", ret_klass->name);
MasterLocal::LocalizeMasterItem(message, ret_klass->name);
}
}
}
/*
// 未使用的 Hook
DEFINE_HOOK(void, MasterBase_GetAll, (void* self, UnityResolve::UnityType::Array<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)) {
auto ret = OctoCaching_GetResourceFileName_Orig(data, method);
//Log::DebugFmt("OctoCaching_GetResourceFileName: %s", ret->ToString().c_str());
@@ -466,20 +528,248 @@ namespace GakumasLocal::HookMain {
// return UnityResolve::UnityType::String::New("[I18]" + ret->ToString());
}
DEFINE_HOOK(void, PictureBookLiveThumbnailView_SetData, (void* self, void* liveData, bool isUnlocked, bool isNew, void* ct, void* mtd)) {
// Log::DebugFmt("PictureBookLiveThumbnailView_SetData: isUnlocked: %d, isNew: %d", isUnlocked, isNew);
/*
DEFINE_HOOK(void*, UserDataManagerBase_get__userIdolCardSkinList, (void* self, void* mtd)) { // Live默认选择
auto ret = UserDataManagerBase_get__userIdolCardSkinList_Orig(self, mtd);
Log::DebugFmt("UserDataManagerBase_get__userIdolCardSkinList: %p", ret);
return ret;
}
DEFINE_HOOK(void*, UserDataManagerBase_get__userCostumeList, (void* self, void* mtd)) { // 服装选择界面
auto ret = UserDataManagerBase_get__userCostumeList_Orig(self, mtd);
Log::DebugFmt("UserDataManagerBase_get__userCostumeList: %p", ret);
return ret;
}
DEFINE_HOOK(void*, UserDataManagerBase_get__userCostumeHeadList, (void* self, void* mtd)) { // 服装选择界面
auto ret = UserDataManagerBase_get__userCostumeHeadList_Orig(self, mtd);
Log::DebugFmt("UserDataManagerBase_get__userCostumeHeadList: %p", ret);
return ret;
}*/
DEFINE_HOOK(bool, UserIdolCardSkinCollection_Exists, (void* self, Il2cppString* id, void* mtd)) { // Live默认选择
auto ret = UserIdolCardSkinCollection_Exists_Orig(self, id, mtd);
// Log::DebugFmt("UserIdolCardSkinCollection_Exists: %s, ret: %d", id->ToString().c_str(), ret);
if (!Config::unlockAllLive) return ret;
if (id) {
std::string idStr = id->ToString();
if (idStr.starts_with("music") || idStr.starts_with("i_card-skin")) { // eg. music-all-kllj-006, i_card-skin-hski-3-002
return true;
}
}
return ret;
}
DEFINE_HOOK(void, PictureBookLiveThumbnailView_SetDataAsync, (void* self, void* liveData, bool isReleased, bool isUnlocked, bool isNew, bool hasLiveSkin, void* ct, void* mtd)) {
// Log::DebugFmt("PictureBookLiveThumbnailView_SetDataAsync: isReleased: %d, isUnlocked: %d, isNew: %d, hasLiveSkin: %d", isReleased, isUnlocked, isNew, hasLiveSkin);
if (Config::dbgMode && Config::unlockAllLive) {
isUnlocked = true;
isReleased = true;
hasLiveSkin = true;
}
PictureBookLiveThumbnailView_SetData_Orig(self, liveData, isUnlocked, isNew, ct, mtd);
PictureBookLiveThumbnailView_SetDataAsync_Orig(self, liveData, isReleased, isUnlocked, isNew, hasLiveSkin, ct, mtd);
}
enum class GetIdolIdType {
MusicId,
CostumeId,
CostumeHeadId
};
std::vector<std::string> GetIdolMusicIdAll(const std::string& charaNameId = "", GetIdolIdType getType = GetIdolIdType::MusicId) {
// 传入例: fktn
// System.Collections.Generic.List`1<valuetype [mscorlib]System.ValueTuple`2<class Campus.Common.Proto.Client.Master.IdolCardSkin, class Campus.Common.Proto.Client.Master.Music>>
static auto get_IdolCardSkinMaster = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "MasterManager", "get_IdolCardSkinMaster");
static auto Master_GetAllWithSortByKey = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "IdolCardSkinMaster", "GetAllWithSortByKey");
static auto IdolCardSkin_get_Id = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_Id");
static auto IdolCardSkin_get_IdolCardId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_IdolCardId");
static auto IdolCardSkin_GetMusic = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "GetMusic");
static auto IdolCardSkin_get_MusicId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_MusicId");
static auto IdolCardSkin_get_CostumeId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_CostumeId");
static auto IdolCardSkin_get_CostumeHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_CostumeHeadId");
static auto GetLiveMusics = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookWindowPresenter", "GetLiveMusics");
auto idolCardSkinMaster = get_IdolCardSkinMaster->Invoke<void*>(nullptr); // IdolCardSkinMaster
std::vector<std::string> ret{};
if (!idolCardSkinMaster) {
Log::ErrorFmt("get_IdolCardSkinMaster failed: %p", idolCardSkinMaster);
return ret;
}
// List<IdolCardSkin>
auto idolCardSkinList = Master_GetAllWithSortByKey->Invoke<UnityResolve::UnityType::List<void*>*>(idolCardSkinMaster, 0x0, nullptr);
auto idolCardSkins = idolCardSkinList->ToArray()->ToVector();
const auto checkStartCharaId = "i_card-" + charaNameId;
// Log::DebugFmt("checkStartCharaId: %s", checkStartCharaId.c_str());
// origMusics->Clear();
UnityResolve::Method* idGetFunc = nullptr;
switch (getType) {
case GetIdolIdType::MusicId: idGetFunc = IdolCardSkin_get_MusicId;
break;
case GetIdolIdType::CostumeId: idGetFunc = IdolCardSkin_get_CostumeId;
break;
case GetIdolIdType::CostumeHeadId: idGetFunc = IdolCardSkin_get_CostumeHeadId;
break;
default:
idGetFunc = IdolCardSkin_get_MusicId;
}
for (auto i : idolCardSkins) {
if (!i) continue;
// auto charaId = IdolCardSkin_get_Id->Invoke<Il2cppString*>(i);
auto targetId = idGetFunc->Invoke<Il2cppString*>(i);
auto cardId = IdolCardSkin_get_IdolCardId->Invoke<Il2cppString*>(i)->ToString();
auto music = IdolCardSkin_GetMusic->Invoke<void*>(i);
if (charaNameId.empty() || cardId.starts_with(checkStartCharaId)) {
std::string musicIdStr = targetId->ToString();
// Log::DebugFmt("Add cardId: %s, musicId: %s", cardId.c_str(), musicIdStr.c_str());
if (std::find(ret.begin(), ret.end(), musicIdStr) == ret.end()) {
ret.emplace_back(musicIdStr);
}
}
}
return ret;
}
void* AddIdsToUserDataCollectionFromMaster(void* origList, std::vector<std::string>& allIds,
UnityResolve::Method* get_CostumeId, UnityResolve::Method* set_CostumeId, UnityResolve::Method* Clone) {
std::unordered_set<std::string> existIds{};
Il2cppUtils::Tools::CSListEditor listEditor(origList);
if (listEditor.get_Count() <= 0) {
return origList;
}
for (auto i : listEditor) {
auto costumeId = get_CostumeId->Invoke<Il2cppString*>(i);
if (!costumeId) continue;
existIds.emplace(costumeId->ToString());
}
for (auto& i : allIds) {
if (i.empty()) continue;
// Log::DebugFmt("Try add %s", i.c_str());
if (existIds.contains(i)) continue;
auto userCostume = Clone->Invoke<void*>(listEditor.get_Item(0));
set_CostumeId->Invoke<void>(userCostume, Il2cppString::New(i));
listEditor.Add(userCostume);
}
return origList;
}
DEFINE_HOOK(void*, UserCostumeCollection_FindBy, (void* self, void* predicate, void* mtd)) {
auto ret = UserCostumeCollection_FindBy_Orig(self, predicate, mtd);
if (!Config::unlockAllLiveCostume) return ret;
auto this_klass = Il2cppUtils::get_class_from_instance(self);
// auto predicate_klass = Il2cppUtils::get_class_from_instance(predicate); // System::Predicate`1
// Log::DebugFmt("UserCostumeCollection_FindBy this: %s::%s, predicate: %s::%s", this_klass->namespaze, this_klass->name,
// predicate_klass->namespaze, predicate_klass->name);
static auto UserCostumeCollection_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User",
"UserCostumeCollection");
static auto UserCostumeCollection_GetAllList_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(
UserCostumeCollection_klass->address, "GetAllList", 1);
static auto UserCostumeCollection_GetAllList = reinterpret_cast<void* (*)(void*, void*)>(UserCostumeCollection_GetAllList_mtd->methodPointer);
std::string thisKlassName(this_klass->name);
// Campus.Common.User::UserCostumeHeadCollection || Campus.Common.User::UserCostumeCollection
// 两个 class 的 GetAllList 均使用的父类 Qua.UserDataManagement.UserDataCollectionBase`2 的方法,地址一致
if (thisKlassName == "UserCostumeHeadCollection") {
static auto UserCostume_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "Clone");
static auto UserCostume_get_CostumeHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "get_CostumeHeadId");
static auto UserCostume_set_CostumeHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "set_CostumeHeadId");
// auto ret_klass = Il2cppUtils::get_class_from_instance(ret); // WhereEnumerableIterator
auto origList = UserCostumeCollection_GetAllList(self, nullptr);
auto allIds = GetIdolMusicIdAll("", GetIdolIdType::CostumeHeadId);
// List<Campus.Common.Proto.Client.Transaction.UserCostumeHead>
return AddIdsToUserDataCollectionFromMaster(origList, allIds, UserCostume_get_CostumeHeadId, UserCostume_set_CostumeHeadId, UserCostume_Clone);
}
else if (thisKlassName == "UserCostumeCollection") {
// static auto UserCostume_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume");
static auto UserCostume_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "Clone");
static auto UserCostume_get_CostumeId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "get_CostumeId");
static auto UserCostume_set_CostumeId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "set_CostumeId");
// auto ret_klass = Il2cppUtils::get_class_from_instance(ret); // WhereEnumerableIterator
auto origList = UserCostumeCollection_GetAllList(self, nullptr);
auto allIds = GetIdolMusicIdAll("", GetIdolIdType::CostumeId);
// List<Campus.Common.Proto.Client.Transaction.UserCostume>
return AddIdsToUserDataCollectionFromMaster(origList, allIds, UserCostume_get_CostumeId, UserCostume_set_CostumeId, UserCostume_Clone);
}
return ret;
}
void* PictureBookWindowPresenter_instance = nullptr;
std::string PictureBookWindowPresenter_charaId;
DEFINE_HOOK(void*, PictureBookWindowPresenter_GetLiveMusics, (void* self, Il2cppString* charaId, void* mtd)) {
// Log::DebugFmt("GetLiveMusics: %s", charaId->ToString().c_str());
if (Config::unlockAllLive) {
PictureBookWindowPresenter_instance = self;
PictureBookWindowPresenter_charaId = charaId->ToString();
static auto PictureBookWindowPresenter_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookWindowPresenter");
static auto existsMusicIds_field = PictureBookWindowPresenter_klass->Get<UnityResolve::Field>("_existsMusicIds");
auto existsMusicIds = Il2cppUtils::ClassGetFieldValue<UnityResolve::UnityType::List<Il2cppString*>*>(self, existsMusicIds_field);
if (!existsMusicIds) {
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);
auto fullIds = GetIdolMusicIdAll();
for (auto& i : fullIds) {
// Log::DebugFmt("GetLiveMusics - Add: %s", i.c_str());
newListEditor.Add(Il2cppString::New(i));
}
Il2cppUtils::ClassSetFieldValue(self, existsMusicIds_field, newList);
// Log::DebugFmt("GetLiveMusics - set end: %d", fullIds.size());
}
}
return PictureBookWindowPresenter_GetLiveMusics_Orig(self, charaId, mtd);
}
DEFINE_HOOK(void, PictureBookLiveSelectScreenModel_ctor, (void* self, void* transitionParam, UnityResolve::UnityType::List<void*>* musics, void* mtd)) {
// Log::DebugFmt("PictureBookLiveSelectScreenModel_ctor");
if (Config::unlockAllLive) {
static auto GetLiveMusics = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookWindowPresenter", "GetLiveMusics");
if (PictureBookWindowPresenter_instance && !PictureBookWindowPresenter_charaId.empty()) {
auto fullMusics = GetLiveMusics->Invoke<UnityResolve::UnityType::List<void*>*>(PictureBookWindowPresenter_instance,
Il2cppString::New(PictureBookWindowPresenter_charaId));
return PictureBookLiveSelectScreenModel_ctor_Orig(self, transitionParam, fullMusics, mtd);
}
}
return PictureBookLiveSelectScreenModel_ctor_Orig(self, transitionParam, musics, mtd);
}
bool needRestoreHides = false;
DEFINE_HOOK(void*, PictureBookLiveSelectScreenPresenter_MoveLiveScene, (void* self, void* produceLive,
Il2cppString* characterId, Il2cppString* costumeId, Il2cppString* costumeHeadId)) {
Il2cppString* characterId, Il2cppString* idolCardId, Il2cppString* costumeId, Il2cppString* costumeHeadId, void* mtd)) {
needRestoreHides = false;
Log::InfoFmt("MoveLiveScene: characterId: %s, costumeId: %s, costumeHeadId: %s,",
characterId->ToString().c_str(), costumeId->ToString().c_str(), costumeHeadId->ToString().c_str());
Log::InfoFmt("MoveLiveScene: characterId: %s, idolCardId: %s, costumeId: %s, costumeHeadId: %s,",
characterId->ToString().c_str(), idolCardId->ToString().c_str(), costumeId->ToString().c_str(), costumeHeadId->ToString().c_str());
/*
characterId: hski, costumeId: hski-cstm-0002, costumeHeadId: costume_head_hski-cstm-0002,
@@ -488,12 +778,13 @@ namespace GakumasLocal::HookMain {
if (Config::dbgMode && Config::enableLiveCustomeDress) {
// 修改 LiveFixedData_GetCharacter 可以更改 Loading 角色和演唱者名字,而不变更实际登台人
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId,
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId, idolCardId,
Config::liveCustomeCostumeId.empty() ? costumeId : Il2cppString::New(Config::liveCustomeCostumeId),
Config::liveCustomeHeadId.empty() ? costumeHeadId : Il2cppString::New(Config::liveCustomeHeadId));
Config::liveCustomeHeadId.empty() ? costumeHeadId : Il2cppString::New(Config::liveCustomeHeadId),
mtd);
}
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId, costumeId, costumeHeadId);
return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, characterId, idolCardId, costumeId, costumeHeadId, mtd);
}
// std::string lastMusicId;
@@ -733,6 +1024,32 @@ namespace GakumasLocal::HookMain {
CampusActorController_LateUpdate_Orig(self, mtd);
}
DEFINE_HOOK(bool, PlatformInformation_get_IsAndroid, ()) {
if (Config::loginAsIOS) {
return false;
}
// Log::DebugFmt("PlatformInformation_get_IsAndroid: 0x%x", ret);
return PlatformInformation_get_IsAndroid_Orig();
}
DEFINE_HOOK(bool, PlatformInformation_get_IsIOS, ()) {
if (Config::loginAsIOS) {
return true;
}
// Log::DebugFmt("PlatformInformation_get_IsIOS: 0x%x", ret);
return PlatformInformation_get_IsIOS_Orig();
}
DEFINE_HOOK(Il2cppString*, ApiBase_GetPlatformString, (void* self, void* mtd)) {
if (Config::loginAsIOS) {
return Il2cppString::New("iOS");
}
// auto ret = ApiBase_GetPlatformString_Orig(self, mtd);
// Log::DebugFmt("ApiBase_GetPlatformString: %s", ret->ToString().c_str());
return ApiBase_GetPlatformString_Orig(self, mtd);
}
void UpdateSwingBreastBonesData(void* initializeData) {
if (!Config::enableBreastParam) return;
static auto CampusActorAnimationInitializeData_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll", "ActorAnimation",
@@ -838,6 +1155,68 @@ namespace GakumasLocal::HookMain {
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() {
const auto hookInstaller = Plugin::GetInstance().GetHookInstaller();
UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW),
@@ -867,6 +1246,39 @@ namespace GakumasLocal::HookMain {
ADD_HOOK(TextField_set_value, Il2cppUtils::GetMethodPointer("UnityEngine.UIElementsModule.dll", "UnityEngine.UIElements",
"TextField", "set_value"));
/* SQL 查询相关函数,不好用
// 下面是 byte[] u8 string 转 std::string 的例子
auto query = reinterpret_cast<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",
"OctoCaching", "GetResourceFileName"));
@@ -880,9 +1292,51 @@ namespace GakumasLocal::HookMain {
Il2cppUtils::GetMethodPointer("Octo.dll", "Octo",
"OnDownloadProgress", "Invoke"));
ADD_HOOK(PictureBookLiveThumbnailView_SetData,
/*
auto UserDataManager_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User",
"UserDataManager");
if (UserDataManager_klass) {
auto UserDataManagerBase_klass = UnityResolve::Invoke<Il2cppUtils::Il2CppClassHead*>("il2cpp_class_get_parent", UserDataManager_klass->address);
if (UserDataManagerBase_klass) {
auto get_userIdolCardSkinList_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(UserDataManagerBase_klass, "get__userIdolCardSkinList", 0);
if (get_userIdolCardSkinList_mtd) {
ADD_HOOK(UserDataManagerBase_get__userIdolCardSkinList, get_userIdolCardSkinList_mtd->methodPointer);
}
auto get_userCostumeList_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(UserDataManagerBase_klass, "get__userCostumeList", 0);
if (get_userCostumeList_mtd) {
ADD_HOOK(UserDataManagerBase_get__userCostumeList, get_userCostumeList_mtd->methodPointer);
}
auto get_userCostumeHeadList_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(UserDataManagerBase_klass, "get__userCostumeHeadList", 0);
if (get_userCostumeHeadList_mtd) {
ADD_HOOK(UserDataManagerBase_get__userCostumeHeadList, get_userCostumeHeadList_mtd->methodPointer);
}
}
}*/
auto UserIdolCardSkinCollection_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User",
"UserIdolCardSkinCollection");
auto UserIdolCardSkinCollection_Exists_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(UserIdolCardSkinCollection_klass->address, "Exists", 1);
if (UserIdolCardSkinCollection_Exists_mtd) {
ADD_HOOK(UserIdolCardSkinCollection_Exists, UserIdolCardSkinCollection_Exists_mtd->methodPointer);
}
auto UserCostumeCollection_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User",
"UserCostumeCollection");
auto UserCostumeCollection_FindBy_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(
UserCostumeCollection_klass->address, "FindBy", 1);
if (UserCostumeCollection_FindBy_mtd) {
ADD_HOOK(UserCostumeCollection_FindBy, UserCostumeCollection_FindBy_mtd->methodPointer);
}
ADD_HOOK(PictureBookLiveThumbnailView_SetDataAsync,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame.PictureBook",
"PictureBookLiveThumbnailView", "SetDataAsync", {"*", "*", "*", "*"}));
"PictureBookLiveThumbnailView", "SetDataAsync", {"*", "*", "*", "*", "*"}));
ADD_HOOK(PictureBookWindowPresenter_GetLiveMusics,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookWindowPresenter", "GetLiveMusics"));
ADD_HOOK(PictureBookLiveSelectScreenModel_ctor,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
"PictureBookLiveSelectScreenModel", ".ctor"));
ADD_HOOK(PictureBookLiveSelectScreenPresenter_MoveLiveScene,
Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
@@ -918,6 +1372,22 @@ namespace GakumasLocal::HookMain {
ADD_HOOK(CampusActorController_LateUpdate,
Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common",
"CampusActorController", "LateUpdate"));
ADD_HOOK(PlatformInformation_get_IsAndroid, Il2cppUtils::GetMethodPointer("Firebase.Platform.dll", "Firebase.Platform",
"PlatformInformation", "get_IsAndroid"));
ADD_HOOK(PlatformInformation_get_IsIOS, Il2cppUtils::GetMethodPointer("Firebase.Platform.dll", "Firebase.Platform",
"PlatformInformation", "get_IsIOS"));
auto api_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Network", "Api");
if (api_klass) {
// Qua.Network.ApiBase
auto api_parent = UnityResolve::Invoke<Il2cppUtils::Il2CppClassHead*>("il2cpp_class_get_parent", api_klass->address);
if (api_parent) {
// Log::DebugFmt("api_parent at %p, name: %s::%s", api_parent, api_parent->namespaze, api_parent->name);
ADD_HOOK(ApiBase_GetPlatformString, Il2cppUtils::il2cpp_class_get_method_from_name(api_parent, "GetPlatformString", 0)->methodPointer);
}
}
/*
static auto CampusActorController_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll",
"Campus.Common", "CampusActorController");
@@ -991,11 +1461,13 @@ namespace GakumasLocal::HookMain {
}
Local::LoadData();
MasterLocal::LoadData();
if (Config::lazyInit) {
UnityResolveProgress::classProgress.current = 1;
UnityResolveProgress::startInit = false;
// UnityResolveProgress::startInit = false;
}
UnityResolveProgress::startInit = false;
Log::Info("Plugin init finished.");
return ret;

View File

@@ -14,14 +14,96 @@ namespace Il2cppUtils {
const char* namespaze;
};
struct Il2CppObject
{
union
{
void* klass;
void* vtable;
};
void* monitor;
};
enum Il2CppTypeEnum
{
IL2CPP_TYPE_END = 0x00, /* End of List */
IL2CPP_TYPE_VOID = 0x01,
IL2CPP_TYPE_BOOLEAN = 0x02,
IL2CPP_TYPE_CHAR = 0x03,
IL2CPP_TYPE_I1 = 0x04,
IL2CPP_TYPE_U1 = 0x05,
IL2CPP_TYPE_I2 = 0x06,
IL2CPP_TYPE_U2 = 0x07,
IL2CPP_TYPE_I4 = 0x08,
IL2CPP_TYPE_U4 = 0x09,
IL2CPP_TYPE_I8 = 0x0a,
IL2CPP_TYPE_U8 = 0x0b,
IL2CPP_TYPE_R4 = 0x0c,
IL2CPP_TYPE_R8 = 0x0d,
IL2CPP_TYPE_STRING = 0x0e,
IL2CPP_TYPE_PTR = 0x0f,
IL2CPP_TYPE_BYREF = 0x10,
IL2CPP_TYPE_VALUETYPE = 0x11,
IL2CPP_TYPE_CLASS = 0x12,
IL2CPP_TYPE_VAR = 0x13,
IL2CPP_TYPE_ARRAY = 0x14,
IL2CPP_TYPE_GENERICINST = 0x15,
IL2CPP_TYPE_TYPEDBYREF = 0x16,
IL2CPP_TYPE_I = 0x18,
IL2CPP_TYPE_U = 0x19,
IL2CPP_TYPE_FNPTR = 0x1b,
IL2CPP_TYPE_OBJECT = 0x1c,
IL2CPP_TYPE_SZARRAY = 0x1d,
IL2CPP_TYPE_MVAR = 0x1e,
IL2CPP_TYPE_CMOD_REQD = 0x1f,
IL2CPP_TYPE_CMOD_OPT = 0x20,
IL2CPP_TYPE_INTERNAL = 0x21,
IL2CPP_TYPE_MODIFIER = 0x40,
IL2CPP_TYPE_SENTINEL = 0x41,
IL2CPP_TYPE_PINNED = 0x45,
IL2CPP_TYPE_ENUM = 0x55
};
typedef struct Il2CppType
{
void* dummy;
unsigned int attrs : 16;
Il2CppTypeEnum type : 8;
unsigned int num_mods : 6;
unsigned int byref : 1;
unsigned int pinned : 1;
} Il2CppType;
struct Il2CppReflectionType
{
Il2CppObject object;
const Il2CppType* type;
};
struct Resolution_t {
int width;
int height;
int herz;
};
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 Il2CppType* return_type;
//const ParameterInfo* parameters;
const void* return_type;
// const void* return_type;
const void* parameters;
uintptr_t methodDefinition;
uintptr_t genericContainer;
@@ -36,13 +118,7 @@ namespace Il2cppUtils {
uint8_t is_marshaled_from_native : 1;
};
struct Resolution_t {
int width;
int height;
int herz;
};
UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName,
static UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className) {
const auto assembly = UnityResolve::Get(assemblyName);
if (!assembly) {
@@ -81,7 +157,7 @@ namespace Il2cppUtils {
return ret;
}*/
UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName,
static UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
const auto assembly = UnityResolve::Get(assemblyName);
if (!assembly) {
@@ -108,7 +184,7 @@ namespace Il2cppUtils {
return method;
}
void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName,
static void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
auto method = GetMethod(assemblyName, nameSpaceName, className, methodName, args);
if (method) {
@@ -117,20 +193,19 @@ namespace Il2cppUtils {
return nullptr;
}
void* il2cpp_resolve_icall(const char* s) {
static void* il2cpp_resolve_icall(const char* s) {
return UnityResolve::Invoke<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)));
}
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);
}
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{};
while (const auto curNestedClass = UnityResolve::Invoke<void*>("il2cpp_class_get_nested_types", klass, &iter))
{
@@ -143,22 +218,175 @@ namespace Il2cppUtils {
return nullptr;
}
void* find_nested_class_from_name(void* klass, const char* name)
{
static void* find_nested_class_from_name(void* klass, const char* name) {
return find_nested_class(klass, [name = std::string_view(name)](void* nestedClass) {
return static_cast<Il2CppClassHead*>(nestedClass)->name == name;
});
}
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);
}
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;
}
static void* get_system_class_from_reflection_type_str(const char* typeStr, const char* assemblyName = "mscorlib") {
using Il2CppString = UnityResolve::UnityType::String;
static auto assemblyLoad = reinterpret_cast<void* (*)(Il2CppString*)>(
GetMethodPointer("mscorlib.dll", "System.Reflection",
"Assembly", "Load", {"*"})
);
static auto assemblyGetType = reinterpret_cast<Il2CppReflectionType * (*)(void*, Il2CppString*)>(
GetMethodPointer("mscorlib.dll", "System.Reflection",
"Assembly", "GetType", {"*"})
);
static auto reflectionAssembly = assemblyLoad(Il2CppString::New(assemblyName));
auto reflectionType = assemblyGetType(reflectionAssembly, Il2CppString::New(typeStr));
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 {
template <typename T = void*>
class CSListEditor {
public:
CSListEditor(void* list) {
list_klass = get_class_from_instance(list);
lst = list;
lst_get_Count_method = il2cpp_class_get_method_from_name(list_klass, "get_Count", 0);
lst_get_Item_method = il2cpp_class_get_method_from_name(list_klass, "get_Item", 1);
lst_set_Item_method = il2cpp_class_get_method_from_name(list_klass, "set_Item", 2);
lst_Add_method = il2cpp_class_get_method_from_name(list_klass, "Add", 1);
lst_get_Count = reinterpret_cast<lst_get_Count_t>(lst_get_Count_method->methodPointer);
lst_get_Item = reinterpret_cast<lst_get_Item_t>(lst_get_Item_method->methodPointer);
lst_set_Item = reinterpret_cast<lst_set_Item_t>(lst_set_Item_method->methodPointer);
lst_Add = reinterpret_cast<lst_Add_t>(lst_Add_method->methodPointer);
}
void Add(T value) {
lst_Add(lst, value, lst_Add_method);
}
T get_Item(int index) {
return lst_get_Item(lst, index, lst_get_Item_method);
}
void set_Item(int index, T value) {
return lst_set_Item(lst, index, value, lst_set_Item_method);
}
int get_Count() {
return lst_get_Count(lst, lst_get_Count_method);
}
T operator[] (int key) {
return get_Item(key);
}
class Iterator {
public:
Iterator(CSListEditor<T>* editor, int index) : editor(editor), index(index) {}
T operator*() const {
return editor->get_Item(index);
}
Iterator& operator++() {
++index;
return *this;
}
bool operator!=(const Iterator& other) const {
return index != other.index;
}
private:
CSListEditor<T>* editor;
int index;
};
Iterator begin() {
return Iterator(this, 0);
}
Iterator end() {
return Iterator(this, get_Count());
}
void* lst;
void* list_klass;
private:
typedef T(*lst_get_Item_t)(void*, int, void* mtd);
typedef void(*lst_Add_t)(void*, T, void* mtd);
typedef void(*lst_set_Item_t)(void*, int, T, void* mtd);
typedef int(*lst_get_Count_t)(void*, void* mtd);
MethodInfo* lst_get_Item_method;
MethodInfo* lst_Add_method;
MethodInfo* lst_get_Count_method;
MethodInfo* lst_set_Item_method;
lst_get_Item_t lst_get_Item;
lst_set_Item_t lst_set_Item;
lst_Add_t lst_Add;
lst_get_Count_t lst_get_Count;
};
}
}

View File

@@ -521,6 +521,17 @@ namespace GakumasLocal::Local {
return false;
}
// 匹配升级卡名
if (auto plusPos = origText.find_last_not_of('+'); plusPos != std::string::npos) {
const auto noPlusText = origText.substr(0, plusPos + 1);
if (const auto iter = genericText.find(noPlusText); iter != genericText.end()) {
size_t plusCount = origText.length() - (plusPos + 1);
*newStr = iter->second + std::string(plusCount, '+');
return true;
}
}
// fmt 文本
auto fmtText = StringParser::ParseItems::parse(origText, false);
if (fmtText.isValid) {

View File

@@ -3,8 +3,11 @@
#include <string>
#include <filesystem>
#include <unordered_set>
namespace GakumasLocal::Local {
extern std::unordered_set<std::string> translatedText;
std::filesystem::path GetBasePath();
void LoadData();
bool GetI18n(const std::string& key, std::string* ret);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -168,6 +168,33 @@ namespace GakumasLocal::Misc {
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, ""};
}
}
}

View File

@@ -76,6 +76,8 @@ namespace GakumasLocal {
namespace StringFormat {
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);
}
}
}

View File

@@ -11,16 +11,20 @@ namespace GakumasLocal::Config {
bool replaceFont = true;
bool forceExportResource = true;
bool textTest = false;
bool useMasterTrans = true;
int gameOrientation = 0;
bool dumpText = false;
bool enableFreeCamera = false;
int targetFrameRate = 0;
bool unlockAllLive = false;
bool unlockAllLiveCostume = false;
bool enableLiveCustomeDress = false;
std::string liveCustomeHeadId = "";
std::string liveCustomeCostumeId = "";
bool loginAsIOS = false;
bool useCustomeGraphicSettings = false;
float renderScale = 0.77f;
int qualitySettingsLevel = 3;
@@ -61,13 +65,16 @@ namespace GakumasLocal::Config {
GetConfigItem(forceExportResource);
GetConfigItem(gameOrientation);
GetConfigItem(textTest);
GetConfigItem(useMasterTrans);
GetConfigItem(dumpText);
GetConfigItem(targetFrameRate);
GetConfigItem(enableFreeCamera);
GetConfigItem(unlockAllLive);
GetConfigItem(unlockAllLiveCostume);
GetConfigItem(enableLiveCustomeDress);
GetConfigItem(liveCustomeHeadId);
GetConfigItem(liveCustomeCostumeId);
GetConfigItem(loginAsIOS);
GetConfigItem(useCustomeGraphicSettings);
GetConfigItem(renderScale);
GetConfigItem(qualitySettingsLevel);

View File

@@ -10,15 +10,19 @@ namespace GakumasLocal::Config {
extern bool forceExportResource;
extern int gameOrientation;
extern bool textTest;
extern bool useMasterTrans;
extern bool dumpText;
extern bool enableFreeCamera;
extern int targetFrameRate;
extern bool unlockAllLive;
extern bool unlockAllLiveCostume;
extern bool enableLiveCustomeDress;
extern std::string liveCustomeHeadId;
extern std::string liveCustomeCostumeId;
extern bool loginAsIOS;
extern bool useCustomeGraphicSettings;
extern float renderScale;
extern int qualitySettingsLevel;

View File

@@ -309,7 +309,7 @@ public:
pDomain = Invoke<void*>("il2cpp_domain_get");
Invoke<void*>("il2cpp_thread_attach", pDomain);
ForeachAssembly();
if (!lazyInit) UnityResolveProgress::startInit = false;
// if (!lazyInit) UnityResolveProgress::startInit = false;
}
else {
pDomain = Invoke<void*>("mono_get_root_domain");

View File

@@ -128,7 +128,15 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
"io.github.chinosk.gakumas.localify.fileprovider",
File(targetFile.absolutePath)
)
intent.setDataAndType(dirUri, "resource/file")
// intent.setDataAndType(dirUri, "resource/file")
grantUriPermission(
"com.bandainamcoent.idolmaster_gakuen",
dirUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
intent.putExtra("resource_file", dirUri)
// intent.clipData = ClipData.newRawUri("resource_file", dirUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}

View File

@@ -17,12 +17,15 @@ import kotlinx.coroutines.runBlocking
interface ConfigListener {
fun onEnabledChanged(value: Boolean)
fun onForceExportResourceChanged(value: Boolean)
fun onLoginAsIOSChanged(value: Boolean)
fun onTextTestChanged(value: Boolean)
fun onUseMasterTransChanged(value: Boolean)
fun onReplaceFontChanged(value: Boolean)
fun onLazyInitChanged(value: Boolean)
fun onEnableFreeCameraChanged(value: Boolean)
fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onUnlockAllLiveChanged(value: Boolean)
fun onUnlockAllLiveCostumeChanged(value: Boolean)
fun onLiveCustomeDressChanged(value: Boolean)
fun onLiveCustomeHeadIdChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onLiveCustomeCostumeIdChanged(s: CharSequence, start: Int, before: Int, count: Int)
@@ -115,6 +118,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onLoginAsIOSChanged(value: Boolean) {
config.loginAsIOS = value
saveConfig()
}
override fun onReplaceFontChanged(value: Boolean) {
config.replaceFont = value
saveConfig()
@@ -131,6 +139,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveConfig()
}
override fun onUseMasterTransChanged(value: Boolean) {
config.useMasterTrans = value
saveConfig()
}
override fun onDumpTextChanged(value: Boolean) {
config.dumpText = value
saveConfig()
@@ -146,6 +159,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveConfig()
}
override fun onUnlockAllLiveCostumeChanged(value: Boolean) {
config.unlockAllLiveCostume = value
saveConfig()
}
override fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int) {
try {
val valueStr = s.toString()

View File

@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
@@ -282,7 +283,14 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
// 使用热更新文件
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
val dataUri = intent.data
// val dataUri = intent.data
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("resource_file", Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>("resource_file")
}
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true

View File

@@ -645,7 +645,7 @@ class PatchActivity : ComponentActivity() {
val copyFilesCmd: MutableList<String> = mutableListOf()
val movedFiles: MutableList<String> = mutableListOf()
savedFileNames.forEach { file ->
val movedFileName = "$installDS/${file}"
val movedFileName = "\"$installDS/${file}\""
movedFiles.add(movedFileName)
val dlSaveFileName = File(targetDirectory, file)
copyFilesCmd.add("$action ${dlSaveFileName.absolutePath} $movedFileName")

View File

@@ -84,7 +84,7 @@ object FilesChecker {
for (i in assets.list(localizationFilesDir)!!) {
if (i.toString() == "version.txt") {
val stream = assets.open("$localizationFilesDir/$i")
return convertToString(stream)
return convertToString(stream).trim()
}
}
return "0.0"
@@ -96,7 +96,7 @@ object FilesChecker {
val versionFile = File(pluginFilesDir, "version.txt")
if (!versionFile.exists()) return "0.0"
return versionFile.readText()
return versionFile.readText().trim()
}
fun convertToString(inputStream: InputStream?): String {
@@ -146,6 +146,7 @@ object FilesChecker {
val genericTransDir = File(localFilesDir, "genericTrans")
val genericTransFile = File(localFilesDir, "generic.json")
val i18nFile = File(localFilesDir, "localization.json")
val masterTransDir = File(localFilesDir, "masterTrans")
if (fontFile.exists()) {
fontFile.delete()
@@ -156,6 +157,9 @@ object FilesChecker {
if (deleteRecursively(genericTransDir)) {
genericTransDir.mkdirs()
}
if (deleteRecursively(masterTransDir)) {
masterTransDir.mkdirs()
}
if (genericTransFile.exists()) {
genericTransFile.writeText("{}")
}

View File

@@ -9,16 +9,20 @@ data class GakumasConfig (
var lazyInit: Boolean = true,
var replaceFont: Boolean = true,
var textTest: Boolean = false,
var useMasterTrans: Boolean = true,
var dumpText: Boolean = false,
var gameOrientation: Int = 0,
var forceExportResource: Boolean = false,
var enableFreeCamera: Boolean = false,
var targetFrameRate: Int = 0,
var unlockAllLive: Boolean = false,
var unlockAllLiveCostume: Boolean = false,
var enableLiveCustomeDress: Boolean = false,
var liveCustomeHeadId: String = "",
var liveCustomeCostumeId: String = "",
var loginAsIOS: Boolean = false,
var useCustomeGraphicSettings: Boolean = false,
var renderScale: Float = 0.77f,
var qualitySettingsLevel: Int = 3,

View File

@@ -75,6 +75,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
item {
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.useMasterDBTrans), checked = config.value.useMasterTrans) {
v -> context?.onUseMasterTransChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) {
v -> context?.onTextTestChanged(v)
}
@@ -86,6 +90,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
v -> context?.onForceExportResourceChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.login_as_ios), checked = config.value.loginAsIOS) {
v -> context?.onLoginAsIOSChanged(v)
}
}
}
@@ -353,6 +361,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
checked = config.value.unlockAllLive) {
v -> context?.onUnlockAllLiveChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.unlockAllLiveCostume),
checked = config.value.unlockAllLiveCostume) {
v -> context?.onUnlockAllLiveCostumeChanged(v)
}
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)

View File

@@ -0,0 +1,131 @@
<resources>
<string name="about">情報</string>
<string name="about_about_p1">このプラグインは完全に無料で提供されます。このプラグインで料金を支払ってしまった場合は、販売者に報告をしてください。</string>
<string name="about_about_p2">プラグインの QQ グループ: 975854705</string>
<string name="about_about_title">このプラグインについて</string>
<string name="about_contributors_asset_file">about_contributors_en.json</string>
<string name="about_warn_p1">このプラグインは学習とコミュニケーションのみを目的としています。</string>
<string name="about_warn_p2">外部プラグインは関連する TOS に違反するため、自己責任でご使用ください。</string>
<string name="about_warn_title">警告</string>
<string name="advanced_settings">高度な設定</string>
<string name="api_addr">APIアドレス (GitHub の最新リリース API)</string>
<string name="app_name">Gakumas Localify</string>
<string name="average">平均</string>
<string name="axisx_x">X 軸.x</string>
<string name="axisx_y">X 軸.y</string>
<string name="axisy_x">Y 軸.x</string>
<string name="axisy_y">Y 軸.y</string>
<string name="axisz_x">Z 軸.x</string>
<string name="axisz_y">Z 軸.y</string>
<string name="basic_settings">基本設定</string>
<string name="breast_param">胸のパラメーター</string>
<string name="breast_scale">胸の大きさ</string>
<string name="camera_settings">カメラ設定</string>
<string name="cancel">キャンセル</string>
<string name="character_counter_content_description">%1$d の %2$d に入力された文字</string>
<string name="character_counter_overflowed_content_description">文字制限が %2$d 文字中、 %1$d 文字を超えています</string>
<string name="character_counter_pattern">%1$d/%2$d</string>
<string name="check_built_in_resource">内蔵アセットのアップデートを確認</string>
<string name="check_resource_from_api">リソースアップデートを API から確認</string>
<string name="check_update">確認</string>
<string name="clear_text_end_icon_content_description">テキストを消去</string>
<string name="close_drawer">ナビゲーションメニューを閉じる</string>
<string name="close_sheet">シートを閉じる</string>
<string name="contributors">貢献者</string>
<string name="damping">ダンプ中</string>
<string name="debug_settings">デバッグ設定</string>
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
<string name="default_error_message">入力が無効です</string>
<string name="default_popup_window_title">ポップアップウィンドウ</string>
<string name="del_remote_after_update">キャッシュファイルをアップデート後に削除</string>
<string name="delete_plugin_resource">プラグインリソースを削除</string>
<string name="download">ダウンロード</string>
<string name="downloaded_resource_version">ダウンロードされたバージョン</string>
<string name="dropdown_menu">ドロップダウンメニュー</string>
<string name="enable_breast_param">胸のパラメーターを有効化</string>
<string name="enable_free_camera">フリーカメラを有効化</string>
<string name="enable_plugin">プラグイン有効化 (ホットリロードなし)</string>
<string name="error_a11y_label">エラー: 無効</string>
<string name="error_icon_content_description">エラー</string>
<string name="export_text">テキストをエクスポート</string>
<string name="exposed_dropdown_menu_content_description">ドロップダウンメニューを表示</string>
<string name="force_export_resource">リソースのアップデートを強制する</string>
<string name="login_as_ios">iOSとしてログイン</string>
<string name="gakumas_localify">Gakumas Localify</string>
<string name="game_patch">ゲームパッチ</string>
<string name="graphic_settings">グラフィック設定</string>
<string name="hign"></string>
<string name="home">ホーム</string>
<string name="home_shizuku_warning">一部の機能が使用できません</string>
<string name="icon_content_description">ダイアログアイコン</string>
<string name="in_progress">実行中</string>
<string name="indeterminate">部分的にチェック済み</string>
<string name="install">インストール</string>
<string name="installing">インストール中</string>
<string name="invalid_zip_file">無効なファイル</string>
<string name="invalid_zip_file_warn">このファイルは有効な ZIP 翻訳リソースパックではありません。</string>
<string name="isdirty">IsDirty</string>
<string name="item_view_role_description">タブ</string>
<string name="lazy_init">高速な初期化 (読み込みを遅延)</string>
<string name="liveUseCustomeDress">ライブのキャラクターをカスタム</string>
<string name="live_costume_head_id">ライブのカスタムヘッド ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">ライブ衣装のカスタム ID (例: hski-cstm-0002)</string>
<string name="low"></string>
<string name="max_high">ウルトラ</string>
<string name="middle"></string>
<string name="off">OFF</string>
<string name="ok">OK</string>
<string name="on">ON</string>
<string name="orientation_landscape">横画面</string>
<string name="orientation_lock">画面を固定</string>
<string name="orientation_orig">オリジナル</string>
<string name="orientation_portrait">縦画面</string>
<string name="password_toggle_content_description">パスワードを表示</string>
<string name="patch_debuggable">デバッグを可能にする</string>
<string name="patch_finished">パッチが完了しました。インストールをしますか?</string>
<string name="patch_integrated">統合</string>
<string name="patch_integrated_desc">"モジュールを埋め込んだ状態なアプリでパッチを当てます。\nパッチを適用したアプリは LSPatch Manager なしで実行できますが、動的に管理はできません。\n統合パッチが適用されたアプリは、LSPatch Manager がインストールされていないデバイスでも使用が可能です。"</string>
<string name="patch_local">ローカル</string>
<string name="patch_local_desc">"モジュールを埋め込まずにアプリにパッチを当てます。\nXposed スコープは再パッチなしで動的に変更が可能です。\nローカルでのパッチを当てたアプリは、ローカルのデバイスでのみ実行可能です。"</string>
<string name="patch_mode">パッチモード</string>
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。\n個人データのバックアップを設定済みであることを確認してください。"</string>
<string name="pendulum">揺れ</string>
<string name="pendulumrange">揺れの範囲</string>
<string name="plugin_code">プラグインのコード</string>
<string name="project_contribution">プロジェクトの貢献者</string>
<string name="range_end">範囲の終了</string>
<string name="range_start">範囲の開始</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="replace_font">フォントを置換する</string>
<string name="reserve_patched">パッチ済みの APK を予約する</string>
<string name="resource_settings">リソース設定</string>
<string name="resource_url">リソース URL</string>
<string name="rootweight">ルートウェイト</string>
<string name="selected">選択済み</string>
<string name="setFpsTitle">最大 FPS (0 はオリジナルの設定を使用します)</string>
<string name="shizuku_available">Shizuku サービスが有効です</string>
<string name="shizuku_unavailable">Shizuku サービスが接続されていません</string>
<string name="spring">跳ね</string>
<string name="start_game">ゲーム開始 / ホットリロードの設定</string>
<string name="stiffness">剛性</string>
<string name="support_file_types">"対応ファイル:\n単一または複数選択: apk\n単一選択: apks、xapk、zip"</string>
<string name="switch_role">切り替え</string>
<string name="tab">タブ</string>
<string name="template_percent">%1$d パーセント。</string>
<string name="test_mode_live">テストモード - ライブ</string>
<string name="text_hook_test_mode">テキストフックテストモード</string>
<string name="useMasterDBTrans">MasterDB をローカライズする</string>
<string name="translation_repository">翻訳のリポジトリ</string>
<string name="translation_resource_update">翻訳リソースをアップデート</string>
<string name="unlockAllLive">すべてのライブを開放</string>
<string name="unlockAllLiveCostume">すべてのライブ衣装を開放</string>
<string name="useCustomeGraphicSettings">カスタムグラフィック設定を使用する</string>
<string name="use_remote_zip_resource">リモート ZIP リソースを使用する</string>
<string name="usearmcorrection">Arm コレクションを使用する</string>
<string name="uselimit_0_1">リミットレンジの倍率 (0 は無制限)</string>
<string name="uselimitmultiplier">乗数制限を使用する</string>
<string name="usescale">胸の大きさを使用する</string>
<string name="very_high">最高</string>
<string name="warning">警告</string>
</resources>

View File

@@ -8,14 +8,17 @@
<string name="start_game">以上述配置启动游戏/重载配置</string>
<string name="setFpsTitle">最大 FPS (0 为保持游戏原设置)</string>
<string name="unlockAllLive">解锁所有 Live</string>
<string name="unlockAllLiveCostume">解锁所有 Live 服装</string>
<string name="liveUseCustomeDress">Live 使用自定义角色</string>
<string name="live_costume_head_id">Live 自定义头部 ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live 自定义服装 ID (例: hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">使用自定义画质设置</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="useMasterDBTrans">使用 MasterDB 本地化</string>
<string name="export_text">导出文本</string>
<string name="force_export_resource">启动后强制导出资源</string>
<string name="login_as_ios">以 iOS 登陆</string>
<string name="max_high">极高</string>
<string name="very_high">超高</string>
<string name="hign"></string>

View File

@@ -8,14 +8,17 @@
<string name="start_game">Start Game / Hot Reload Config</string>
<string name="setFpsTitle">Max FPS (0 is Use Original Settings)</string>
<string name="unlockAllLive">Unlock All Live</string>
<string name="unlockAllLiveCostume">Unlock All Live Costume</string>
<string name="liveUseCustomeDress">Live Custom Character</string>
<string name="live_costume_head_id">Live Custom Head ID (eg. costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live Custom Dress ID (eg. hski-cstm-0002)</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="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="force_export_resource">Force Update Resource</string>
<string name="login_as_ios">Login as iOS</string>
<string name="max_high">Ultra</string>
<string name="very_high">Very High</string>
<string name="hign">High</string>

View File

@@ -17,7 +17,7 @@ lifecycle = "2.8.2"
material = "1.12.0"
navigationCompose = "2.7.7"
xdl = "2.1.1"
shadowhook = "1.0.9"
shadowhook = "1.0.10"
serialization="1.7.1"
zip4j = "2.9.1"