23 Commits
v1.6 ... v2.0.2

Author SHA1 Message Date
chinosk
0218543935 fixed LoginAsIOS not working in 1.8.0 2025-01-20 20:13:07 +00:00
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
chinosk
f82e73845a delete cache 2024-08-09 20:36:58 +08:00
chinosk
8ddd6f53bc Compatible with Android 10 2024-08-09 20:15:21 +08:00
38 changed files with 2878 additions and 59 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"
versionCode 10
versionName "v2.0.2"
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

@@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"

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,77 @@ 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");
}
// Log::DebugFmt("ApiBase_GetPlatformString: %s", ret->ToString().c_str());
return ApiBase_GetPlatformString_Orig(self, mtd);
}
void ProcessApiBase(void* self) {
static void* processedIOS = nullptr;
if (Config::loginAsIOS) {
if (self == processedIOS) return;
static auto ApiBase_klass = Il2cppUtils::get_class_from_instance(self);
static auto platform_field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>("il2cpp_class_get_field_from_name", ApiBase_klass, "_platform");
auto platform = Il2cppUtils::ClassGetFieldValue<Il2cppString*>(self, platform_field);
Log::DebugFmt("ProcessApiBase platform: %s", platform ? platform->ToString().c_str() : "null");
if (platform) {
const auto origPlatform = platform->ToString();
if (origPlatform != "iOS") {
Il2cppUtils::ClassSetFieldValue(self, platform_field, Il2cppString::New("iOS"));
processedIOS = self;
}
}
else {
Il2cppUtils::ClassSetFieldValue(self, platform_field, Il2cppString::New("iOS"));
processedIOS = self;
}
}
else {
if (processedIOS) {
Log::DebugFmt("Restore API to Android");
static auto ApiBase_klass = Il2cppUtils::get_class_from_instance(self);
static auto platform_field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>("il2cpp_class_get_field_from_name", ApiBase_klass, "_platform");
Il2cppUtils::ClassSetFieldValue(self, platform_field, Il2cppString::New("Android"));
processedIOS = nullptr;
}
}
}
DEFINE_HOOK(void, ApiBase_ctor, (void* self, void* mtd)) {
ApiBase_ctor_Orig(self, mtd);
ProcessApiBase(self);
}
DEFINE_HOOK(void*, ApiBase_get_Instance, (void* mtd)) {
auto ret = ApiBase_get_Instance_Orig(mtd);
if (ret) {
ProcessApiBase(ret);
}
return ret;
}
void UpdateSwingBreastBonesData(void* initializeData) {
if (!Config::enableBreastParam) return;
static auto CampusActorAnimationInitializeData_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll", "ActorAnimation",
@@ -838,6 +1200,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 +1291,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 +1337,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 +1417,24 @@ 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_pointer_from_name(api_parent, "GetPlatformString", 0));
ADD_HOOK(ApiBase_ctor, Il2cppUtils::il2cpp_class_get_method_pointer_from_name(api_parent, ".ctor", 0));
ADD_HOOK(ApiBase_get_Instance, Il2cppUtils::il2cpp_class_get_method_pointer_from_name(api_parent, "get_Instance", 0));
}
}
/*
static auto CampusActorController_klass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll",
"Campus.Common", "CampusActorController");
@@ -991,11 +1508,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,28 @@ 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 uintptr_t il2cpp_class_get_method_pointer_from_name(void* klass, const char* name, int argsCount) {
auto findKlass = il2cpp_class_get_method_from_name(klass, name, argsCount);
if (findKlass) {
return findKlass->methodPointer;
}
Log::ErrorFmt("method: %s not found", name);
return 0;
}
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 +227,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

@@ -1,19 +1,29 @@
package io.github.chinosk.gakumas.localify
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import io.github.chinosk.gakumas.localify.mainUtils.IOnShell
import io.github.chinosk.gakumas.localify.mainUtils.LSPatchUtils
@@ -29,11 +39,13 @@ import kotlinx.coroutines.withContext
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermissions
import java.util.concurrent.CountDownLatch
interface PatchCallback {
@@ -99,6 +111,137 @@ class PatchActivity : ComponentActivity() {
private var reservePatchFiles: Boolean = false
var patchCallback: PatchCallback? = null
private val writePermissionLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != RESULT_OK) {
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
finish()
}
}
private val writePermissionLauncherQ = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) {
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
finish()
}
}
private fun checkAndRequestWritePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
/*
// 针对 API 级别 30 及以上使用 MediaStore.createWriteRequest
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val intentSender = MediaStore.createWriteRequest(contentResolver, listOf(uri)).intentSender
writePermissionLauncher.launch(IntentSenderRequest.Builder(intentSender).build())*/
}
else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 请求 WRITE_EXTERNAL_STORAGE 权限
writePermissionLauncherQ.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
private fun writeFileToDownloadFolder(
sourceFile: File,
targetFolder: String,
targetFileName: String
): Boolean {
val downloadDirectory = Environment.DIRECTORY_DOWNLOADS
val relativePath = "$downloadDirectory/$targetFolder/"
val resolver = contentResolver
// 检查文件是否已经存在
val existingUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val query = resolver.query(
existingUri,
arrayOf(MediaStore.Files.FileColumns._ID),
"${MediaStore.Files.FileColumns.RELATIVE_PATH}=? AND ${MediaStore.Files.FileColumns.DISPLAY_NAME}=?",
arrayOf(relativePath, targetFileName),
null
)
query?.use {
if (it.moveToFirst()) {
// 如果文件存在,则删除
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
val deleteUri = MediaStore.Files.getContentUri("external", id)
resolver.delete(deleteUri, null, null)
Log.d(patchTag, "query delete: $deleteUri")
}
}
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, targetFileName)
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
put(MediaStore.Downloads.RELATIVE_PATH, relativePath)
}
var uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
Log.d(patchTag, "insert uri: $uri")
if (uri == null) {
val latch = CountDownLatch(1)
val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val downloadSaveDirectory = File(downloadDirectory, targetFolder)
val downloadSaveFile = File(downloadSaveDirectory, targetFileName)
MediaScannerConnection.scanFile(this, arrayOf(downloadSaveFile.absolutePath),
null
) { _, _ ->
Log.d(patchTag, "scanFile finished.")
latch.countDown()
}
latch.await()
uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri == null) {
Log.e(patchTag, "uri is still null")
return false
}
}
return try {
resolver.openOutputStream(uri)?.use { outputStream ->
FileInputStream(sourceFile).use { inputStream ->
inputStream.copyTo(outputStream)
}
}
contentValues.clear()
contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
true
} catch (e: Exception) {
resolver.delete(uri, null, null)
e.printStackTrace()
false
}
}
private fun deleteFileInDownloadFolder(targetFolder: String, targetFileName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
val selectionArgs =
arrayOf("${Environment.DIRECTORY_DOWNLOADS}/$targetFolder/", targetFileName)
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
contentResolver.delete(uri, selection, selectionArgs)
}
else {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "$targetFolder/$targetFileName")
if (file.exists()) {
if (file.delete()) {
// Toast.makeText(this, "文件已删除", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun handleSelectedFile(uri: Uri) {
val fileName = uri.path?.substringAfterLast('/')
if (fileName != null) {
@@ -110,6 +253,7 @@ class PatchActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
outputDir = "${filesDir.absolutePath}/output"
// ShizukuApi.init()
checkAndRequestWritePermission()
setContent {
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
@@ -414,7 +558,38 @@ class PatchActivity : ComponentActivity() {
return movedFiles
}
suspend fun installSplitApks(context: Context, apkFiles: List<File>, reservePatchFiles: Boolean,
private fun generateNonce(size: Int): String {
val nonceScope = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
val scopeSize = nonceScope.length
val nonceItem: (Int) -> Char = { nonceScope[(scopeSize * Math.random()).toInt()] }
return Array(size, nonceItem).joinToString("")
}
fun saveFilesToDownload(context: PatchActivity, apkFiles: List<File>, targetFolder: String,
isMove: Boolean): List<String>? {
val ret: MutableList<String> = mutableListOf()
apkFiles.forEach { f ->
val success = context.writeFileToDownloadFolder(f, targetFolder, f.name)
if (success) {
ret.add(f.name)
}
else {
val newName = "${generateNonce(6)}${f.name}"
val success2 = context.writeFileToDownloadFolder(f, targetFolder,
newName)
if (!success2) {
return null
}
ret.add(newName)
}
if (isMove) {
f.delete()
}
}
return ret
}
suspend fun installSplitApks(context: PatchActivity, apkFiles: List<File>, reservePatchFiles: Boolean,
patchCallback: PatchCallback?): Pair<Int, String?> {
Log.i(TAG, "Perform install patched apks")
var status = PackageInstaller.STATUS_FAILURE
@@ -424,13 +599,27 @@ class PatchActivity : ComponentActivity() {
runCatching {
val sdcardPath = Environment.getExternalStorageDirectory().path
val targetDirectory = File(sdcardPath, "Download/gkms_local_patch")
val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
patchCallback?.onLog("Patched files: $savedFiles")
// val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
val savedFileNames = saveFilesToDownload(context, apkFiles, "gkms_local_patch", true)
if (savedFileNames == null) {
status = PackageInstaller.STATUS_FAILURE
message = "Save files failed."
return@runCatching
}
// patchCallback?.onLog("Patched files: $savedFiles")
patchCallback?.onLog("Patched files: $apkFiles")
if (!ShizukuApi.isPermissionGranted) {
status = PackageInstaller.STATUS_FAILURE
message = "Shizuku Not Ready."
if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
// if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
if (!reservePatchFiles) {
savedFileNames.forEach { f ->
context.deleteFileInDownloadFolder("gkms_local_patch", f)
}
}
return@runCatching
}
@@ -455,16 +644,26 @@ class PatchActivity : ComponentActivity() {
val action = if (reservePatchFiles) "cp" else "mv"
val copyFilesCmd: MutableList<String> = mutableListOf()
val movedFiles: MutableList<String> = mutableListOf()
savedFileNames.forEach { file ->
val movedFileName = "\"$installDS/${file}\""
movedFiles.add(movedFileName)
val dlSaveFileName = File(targetDirectory, file)
copyFilesCmd.add("$action ${dlSaveFileName.absolutePath} $movedFileName")
}
/*
savedFiles.forEach { file ->
val movedFileName = "$installDS/${file.name}"
movedFiles.add(movedFileName)
copyFilesCmd.add("$action ${file.absolutePath} $movedFileName")
}
val moveFileCommand = "mkdir $installDS && " +
"chmod 777 $installDS && " +
*/
val createDirCommand = "mkdir $installDS"
val moveFileCommand = "chmod 777 $installDS && " +
copyFilesCmd.joinToString(" && ")
Log.d(TAG, "moveFileCommand: $moveFileCommand")
ShizukuShell(mutableListOf(), createDirCommand, ioShell).exec().destroy()
val cpFileShell = ShizukuShell(mutableListOf(), moveFileCommand, ioShell)
cpFileShell.exec()
cpFileShell.destroy()

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

@@ -23,7 +23,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
val isBusy: Boolean
get() = mOutput.size > 0 && mOutput[mOutput.size - 1] != "aShell: Finish"
fun exec() {
fun exec(): ShizukuShell {
try {
Log.i(shellTag, "Execute: $mCommand")
shellCallback?.onShellLine(mCommand)
@@ -66,6 +66,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
mProcess!!.waitFor()
} catch (ignored: Exception) {
}
return this
}
fun destroy() {

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

@@ -27,7 +27,7 @@ import java.io.File
@Composable
fun InstallDiag(context: Context?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
fun InstallDiag(context: PatchActivity?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
onFinish: (Int, String?) -> Unit) {
// val scope = rememberCoroutineScope()
// var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }

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"