Compare commits

..

12 Commits
v3.1.1 ... main

19 changed files with 536 additions and 268 deletions

View File

@ -3,6 +3,8 @@
- 学园偶像大师 本地化插件
- **开发中**
- 下游更改将API版本提升至101并暂时禁用Patcher
# Usage

View File

@ -16,7 +16,7 @@ android {
minSdk 29
targetSdk 34
versionCode 12
versionName "v3.1.1"
versionName "v3.2.0"
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -130,6 +130,6 @@ dependencies {
implementation(libs.xdl)
implementation(libs.shadowhook)
compileOnly(libs.xposed.api)
compileOnly(libs.libxposed.api)
implementation(libs.kotlinx.serialization.json)
}

View File

@ -21,23 +21,6 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="IDOLM@STER Gakuen localify" />
<meta-data
android:name="xposedminversion"
android:value="53" />
<meta-data
android:name="xposedsharedprefs"
android:value="true" />
<meta-data
android:name="xposedscope"
android:value="com.bandainamcoent.idolmaster_gakuen" />
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -1 +0,0 @@
io.github.chinosk.gakumas.localify.GakumasHookMain

View File

@ -68,4 +68,8 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
log
fmt)
if (ANDROID AND ANDROID_PLATFORM_LEVEL GREATER_EQUAL 31)
target_link_libraries(${CMAKE_PROJECT_NAME} icu)
endif()
target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_23)

View File

@ -6,6 +6,7 @@
#include "Local.h"
#include "MasterLocal.h"
#include <unordered_set>
#include <algorithm>
#include "camera/camera.hpp"
#include "config/Config.hpp"
// #include <jni.h>
@ -466,6 +467,8 @@ namespace GakumasLocal::HookMain {
"TMPro", "TMP_Text", "get_font");
static auto set_font = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll",
"TMPro", "TMP_Text", "set_font");
static auto get_name = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll",
"UnityEngine", "Object", "get_name");
// static auto set_fontMaterial = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll",
// "TMPro", "TMP_Text", "set_fontMaterial");
// static auto ForceMeshUpdate = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll",
@ -479,21 +482,32 @@ namespace GakumasLocal::HookMain {
static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
"TMP_FontAsset", "UpdateFontAssetData");
auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
if (!fontAsset) {
return;
}
// 检查字体名称,跳过 CampusAlphanumeric 系列字体
auto fontAssetName = get_name->Invoke<Il2cppString*>(fontAsset);
if (fontAssetName) {
std::string fontName = fontAssetName->ToString();
std::transform(fontName.begin(), fontName.end(), fontName.begin(),
[](unsigned char c) { return std::tolower(c); });
if (fontName.find("campusalphanumeric") != std::string::npos) {
return; // 保持原版数字字体
}
}
auto newFont = GetReplaceFont();
if (!newFont) return;
auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
if (fontAsset) {
set_sourceFontFile->Invoke<void>(fontAsset, newFont);
if (!updatedFontPtrs.contains(fontAsset)) {
updatedFontPtrs.emplace(fontAsset);
UpdateFontAssetData->Invoke<void>(fontAsset);
}
if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear();
}
else {
Log::Error("UpdateFont: fontAsset is null.");
set_sourceFontFile->Invoke<void>(fontAsset, newFont);
if (!updatedFontPtrs.contains(fontAsset)) {
updatedFontPtrs.emplace(fontAsset);
UpdateFontAssetData->Invoke<void>(fontAsset);
}
if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear();
set_font->Invoke<void>(TMP_Textself, fontAsset);
// auto fontMaterial = get_material->Invoke<void*>(fontAsset);
@ -526,6 +540,46 @@ namespace GakumasLocal::HookMain {
UpdateFont(self);
}
DEFINE_HOOK(void, TMP_Text_set_text, (void* self, Il2cppString* value, void* mtd)) {
if (!value) {
return TMP_Text_set_text_Orig(self, value, mtd);
}
const std::string origText = value->ToString();
std::string transText;
if (Local::GetGenericText(origText, &transText)) {
const auto newText = UnityResolve::UnityType::String::New(transText);
UpdateFont(self);
return TMP_Text_set_text_Orig(self, newText, mtd);
}
if (Config::textTest) {
TMP_Text_set_text_Orig(self, UnityResolve::UnityType::String::New("[TT]" + origText), mtd);
}
else {
TMP_Text_set_text_Orig(self, value, mtd);
}
UpdateFont(self);
}
DEFINE_HOOK(void, TMP_Text_SetText_1, (void* self, Il2cppString* sourceText, void* mtd)) {
if (!sourceText) {
return TMP_Text_SetText_1_Orig(self, sourceText, mtd);
}
const std::string origText = sourceText->ToString();
std::string transText;
if (Local::GetGenericText(origText, &transText)) {
const auto newText = UnityResolve::UnityType::String::New(transText);
UpdateFont(self);
return TMP_Text_SetText_1_Orig(self, newText, mtd);
}
if (Config::textTest) {
TMP_Text_SetText_1_Orig(self, UnityResolve::UnityType::String::New("[T1]" + origText), mtd);
}
else {
TMP_Text_SetText_1_Orig(self, sourceText, mtd);
}
UpdateFont(self);
}
DEFINE_HOOK(void, TMP_Text_SetText_2, (void* self, Il2cppString* sourceText, bool syncTextInputBox, void* mtd)) {
if (!sourceText) {
return TMP_Text_SetText_2_Orig(self, sourceText, syncTextInputBox, mtd);
@ -572,9 +626,64 @@ namespace GakumasLocal::HookMain {
TextMeshProUGUI_Awake_Orig(self, method);
}
// TODO 文本未hook完整
// Legacy UnityEngine.UI.Text hook礼物/邮件等非TMP界面
DEFINE_HOOK(void, UIText_set_text, (void* self, Il2cppString* value)) {
if (!value) {
return UIText_set_text_Orig(self, value);
}
const std::string origText = value->ToString();
std::string transText;
if (Local::GetGenericText(origText, &transText)) {
const auto newText = UnityResolve::UnityType::String::New(transText);
return UIText_set_text_Orig(self, newText);
}
if (Config::textTest) {
UIText_set_text_Orig(self, UnityResolve::UnityType::String::New("[UI]" + origText));
}
else {
UIText_set_text_Orig(self, value);
}
}
// TMP_Text.SetCharArray(char[], int, int) — 礼物/邮件描述文字通过此路径设置
DEFINE_HOOK(void, TMP_Text_SetCharArray, (void* self, void* charArray, int start, int count, void* mtd)) {
if (charArray && start >= 0 && count > 0) {
// IL2CPP char[] elements are uint16_t (UTF-16)
auto arr = reinterpret_cast<UnityResolve::UnityType::Array<uint16_t>*>(charArray);
// 边界检查:确保 start+count 不超出数组长度
if (static_cast<uintptr_t>(start + count) <= arr->max_length) {
auto rawData = arr->GetData();
if (rawData) {
// rawData 是 uintptr_t字节地址每个 char16_t 占 2 字节
// 必须用 start * sizeof(char16_t) 而非直接 + start否则偏移量减半
const std::u16string u16(
reinterpret_cast<const char16_t*>(rawData + static_cast<uintptr_t>(start) * sizeof(char16_t)),
static_cast<size_t>(count));
const std::string origText = Misc::ToUTF8(u16);
std::string transText;
if (Local::GetGenericText(origText, &transText)) {
UpdateFont(self);
TMP_Text_set_text_Orig(self, Il2cppString::New(transText), nullptr);
return;
}
if (Config::textTest) {
UpdateFont(self);
TMP_Text_set_text_Orig(self, Il2cppString::New("[CA]" + origText), nullptr);
return;
}
}
}
}
TMP_Text_SetCharArray_Orig(self, charArray, start, count, mtd);
}
DEFINE_HOOK(void, TextField_set_value, (void* self, Il2cppString* value)) {
Log::DebugFmt("TextField_set_value: %s", value->ToString().c_str());
if (value) {
std::string transText;
if (Local::GetGenericText(value->ToString(), &transText)) {
return TextField_set_value_Orig(self, UnityResolve::UnityType::String::New(transText));
}
}
TextField_set_value_Orig(self, value);
}
@ -1597,6 +1706,11 @@ namespace GakumasLocal::HookMain {
ADD_HOOK(TextMeshProUGUI_Awake, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro",
"TextMeshProUGUI", "Awake"));
ADD_HOOK(TMP_Text_set_text, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro",
"TMP_Text", "set_text"));
ADD_HOOK(TMP_Text_SetText_1, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro",
"TMP_Text", "SetText",
{"System.String"}));
ADD_HOOK(TMP_Text_PopulateTextBackingArray, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro",
"TMP_Text", "PopulateTextBackingArray",
{"System.String", "System.Int32", "System.Int32"}));
@ -1606,6 +1720,21 @@ namespace GakumasLocal::HookMain {
ADD_HOOK(TextField_set_value, Il2cppUtils::GetMethodPointer("UnityEngine.UIElementsModule.dll", "UnityEngine.UIElements",
"TextField", "set_value"));
// Legacy UnityEngine.UI.Text hook
{
auto uiTextPtr = Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI",
"Text", "set_text");
if (uiTextPtr) {
ADD_HOOK(UIText_set_text, uiTextPtr);
}
else {
Log::InfoFmt("UIText_set_text: method not found, legacy UI.Text hook skipped.");
}
}
ADD_HOOK(TMP_Text_SetCharArray, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro",
"TMP_Text", "SetCharArray", {"System.Char[]", "System.Int32", "System.Int32"}));
/* SQL 查询相关函数,不好用
// 下面是 byte[] u8 string 转 std::string 的例子
auto query = reinterpret_cast<UnityResolve::UnityType::Array<UnityResolve::UnityType::Byte>*>(mtd);

View File

@ -1,11 +1,12 @@
#include "Misc.hpp"
#include <codecvt>
#include <locale>
#include "fmt/core.h"
#ifndef GKMS_WINDOWS
#include <jni.h>
#if defined(__ANDROID__) && __ANDROID_API__ >= 31
#include <unicode/ustring.h>
#endif
extern JavaVM* g_javaVM;
#else
@ -32,14 +33,136 @@ namespace GakumasLocal::Misc {
return utility::conversions::utf16_to_utf8(wstr);
}
#else
namespace {
jclass GetStringClass(JNIEnv* env) {
static jclass stringClass = nullptr;
if (stringClass) return stringClass;
jclass localClass = env->FindClass("java/lang/String");
if (!localClass) return nullptr;
stringClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
env->DeleteLocalRef(localClass);
return stringClass;
}
jstring GetUtf8CharsetName(JNIEnv* env) {
static jstring utf8Charset = nullptr;
if (utf8Charset) return utf8Charset;
jstring localUtf8 = env->NewStringUTF("UTF-8");
if (!localUtf8) return nullptr;
utf8Charset = reinterpret_cast<jstring>(env->NewGlobalRef(localUtf8));
env->DeleteLocalRef(localUtf8);
return utf8Charset;
}
}
std::u16string ToUTF16(const std::string_view& str) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
return utf16conv.from_bytes(str.data(), str.data() + str.size());
#if defined(__ANDROID__) && __ANDROID_API__ >= 31
UErrorCode status = U_ZERO_ERROR;
int32_t outLen = 0;
u_strFromUTF8(nullptr, 0, &outLen, str.data(), static_cast<int32_t>(str.size()), &status);
if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) return {};
status = U_ZERO_ERROR;
std::u16string out(outLen, u'\0');
u_strFromUTF8(
reinterpret_cast<UChar*>(out.data()),
outLen,
&outLen,
str.data(),
static_cast<int32_t>(str.size()),
&status);
if (U_FAILURE(status)) return {};
out.resize(outLen);
return out;
#else
JNIEnv* env = GetJNIEnv();
if (!env) return {};
jclass stringClass = GetStringClass(env);
jstring utf8Charset = GetUtf8CharsetName(env);
if (!stringClass || !utf8Charset) return {};
jmethodID ctor = env->GetMethodID(stringClass, "<init>", "([BLjava/lang/String;)V");
if (!ctor) return {};
jbyteArray bytes = env->NewByteArray(static_cast<jsize>(str.size()));
if (!bytes) return {};
env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(str.size()), reinterpret_cast<const jbyte*>(str.data()));
jstring jstr = reinterpret_cast<jstring>(env->NewObject(stringClass, ctor, bytes, utf8Charset));
env->DeleteLocalRef(bytes);
if (!jstr || env->ExceptionCheck()) {
env->ExceptionClear();
if (jstr) env->DeleteLocalRef(jstr);
return {};
}
const jsize len = env->GetStringLength(jstr);
const jchar* chars = env->GetStringChars(jstr, nullptr);
if (!chars) {
env->DeleteLocalRef(jstr);
return {};
}
std::u16string out(reinterpret_cast<const char16_t*>(chars), reinterpret_cast<const char16_t*>(chars) + len);
env->ReleaseStringChars(jstr, chars);
env->DeleteLocalRef(jstr);
return out;
#endif
}
std::string ToUTF8(const std::u16string_view& str) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
return utf16conv.to_bytes(str.data(), str.data() + str.size());
#if defined(__ANDROID__) && __ANDROID_API__ >= 31
UErrorCode status = U_ZERO_ERROR;
int32_t outLen = 0;
u_strToUTF8(nullptr, 0, &outLen, reinterpret_cast<const UChar*>(str.data()), static_cast<int32_t>(str.size()), &status);
if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) return {};
status = U_ZERO_ERROR;
std::string out(outLen, '\0');
u_strToUTF8(
out.data(),
outLen,
&outLen,
reinterpret_cast<const UChar*>(str.data()),
static_cast<int32_t>(str.size()),
&status);
if (U_FAILURE(status)) return {};
out.resize(outLen);
return out;
#else
JNIEnv* env = GetJNIEnv();
if (!env) return {};
jclass stringClass = GetStringClass(env);
jstring utf8Charset = GetUtf8CharsetName(env);
if (!stringClass || !utf8Charset) return {};
jstring jstr = env->NewString(reinterpret_cast<const jchar*>(str.data()), static_cast<jsize>(str.size()));
if (!jstr) return {};
jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
if (!getBytes) {
env->DeleteLocalRef(jstr);
return {};
}
jbyteArray bytes = reinterpret_cast<jbyteArray>(env->CallObjectMethod(jstr, getBytes, utf8Charset));
env->DeleteLocalRef(jstr);
if (!bytes || env->ExceptionCheck()) {
env->ExceptionClear();
if (bytes) env->DeleteLocalRef(bytes);
return {};
}
const jsize len = env->GetArrayLength(bytes);
std::string out(static_cast<size_t>(len), '\0');
env->GetByteArrayRegion(bytes, 0, len, reinterpret_cast<jbyte*>(out.data()));
env->DeleteLocalRef(bytes);
return out;
#endif
}
#endif

View File

@ -565,10 +565,10 @@ public:
}
catch (...) {
std::cout << funcName << " Invoke Error\n";
Return();
return Return();
}
}
Return();
return Return();
}
inline static std::vector<Assembly*> assembly;

View File

@ -3,7 +3,7 @@ package io.github.chinosk.gakumas.localify
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.AndroidAppHelper
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@ -17,34 +17,32 @@ import android.view.MotionEvent
import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.ui.game_attach.InitProgressUI
import io.github.libxposed.api.XposedInterface
import io.github.libxposed.api.XposedModule
import io.github.libxposed.api.XposedModuleInterface
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File
import java.lang.reflect.Method
import java.util.Locale
import kotlin.system.measureTimeMillis
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.ui.game_attach.InitProgressUI
val TAG = "GakumasLocalify"
class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private lateinit var modulePath: String
private var nativeLibLoadSuccess: Boolean
class GakumasHookMain : XposedModule() {
private var modulePath: String = ""
private var nativeLibLoadSuccess: Boolean = false
private var alreadyInitialized = false
private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
private val nativeLibName = "MarryKotone"
@ -55,160 +53,197 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var externalFilesChecked: Boolean = false
private var gameActivity: Activity? = null
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") {
// XposedHelpers.findAndHookMethod(
// "io.github.chinosk.gakumas.localify.MainActivity",
// lpparam.classLoader,
// "showToast",
// String::class.java,
// object : XC_MethodHook() {
// override fun beforeHookedMethod(param: MethodHookParam) {
// Log.d(TAG, "beforeHookedMethod hooked: ${param.args}")
// }
// }
// )
// }
override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
modulePath = getModuleApplicationInfo().sourceDir
if (lpparam.packageName != targetPackageName) {
ShadowHook.init(
ConfigBuilder()
.setMode(ShadowHook.Mode.UNIQUE)
.build()
)
nativeLibLoadSuccess = try {
System.loadLibrary(nativeLibName)
true
} catch (_: UnsatisfiedLinkError) {
false
}
}
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
if (param.packageName != targetPackageName) {
return
}
XposedHelpers.findAndHookMethod(
"android.app.Activity",
lpparam.classLoader,
"dispatchKeyEvent",
KeyEvent::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val keyEvent = param.args[0] as KeyEvent
val keyCode = keyEvent.keyCode
val action = keyEvent.action
// Log.d(TAG, "Key event: keyCode=$keyCode, action=$action")
keyboardEvent(keyCode, action)
}
val classLoader = param.classLoader
hookMethod(
classLoader = classLoader,
className = "android.app.Activity",
methodName = "dispatchKeyEvent",
parameterTypes = arrayOf(KeyEvent::class.java),
before = { chain ->
val keyEvent = chain.getArg(0) as KeyEvent
keyboardEvent(keyEvent.keyCode, keyEvent.action)
}
)
XposedHelpers.findAndHookMethod(
"android.app.Activity",
lpparam.classLoader,
"dispatchGenericMotionEvent",
MotionEvent::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val motionEvent = param.args[0] as MotionEvent
val action = motionEvent.action
hookMethod(
classLoader = classLoader,
className = "android.app.Activity",
methodName = "dispatchGenericMotionEvent",
parameterTypes = arrayOf(MotionEvent::class.java),
before = { chain ->
val motionEvent = chain.getArg(0) as MotionEvent
val action = motionEvent.action
// 左摇杆的X和Y轴
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
// 左摇杆的X和Y轴
val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
// 右摇杆的X和Y轴
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
// 右摇杆的X和Y轴
val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
// 左扳机
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
// 左扳机
val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
// 右扳机
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
// 右扳机
val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
// 十字键
val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
// 十字键
val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)
// 处理摇杆和扳机事件
joystickEvent(
action,
leftStickX,
leftStickY,
rightStickX,
rightStickY,
leftTrigger,
rightTrigger,
hatX,
hatY
)
}
// 处理摇杆和扳机事件
joystickEvent(
action,
leftStickX,
leftStickY,
rightStickX,
rightStickY,
leftTrigger,
rightTrigger,
hatX,
hatY
)
}
)
val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader)
XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
Log.d(TAG, "onStart")
val currActivity = param.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
}
else {
initGkmsConfig(currActivity)
}
val activityClass = classLoader.loadClass("android.app.Activity")
hookAllMethods(activityClass, "onStart") { chain ->
Log.d(TAG, "onStart")
val currActivity = chain.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
} else {
initGkmsConfig(currActivity)
}
})
chain.proceed()
}
XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
Log.d(TAG, "onResume")
val currActivity = param.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
}
else {
initGkmsConfig(currActivity)
}
hookAllMethods(activityClass, "onResume") { chain ->
Log.d(TAG, "onResume")
val currActivity = chain.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
} else {
initGkmsConfig(currActivity)
}
})
chain.proceed()
}
val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer")
XposedHelpers.findAndHookMethod(
cls,
"loadNative",
String::class.java,
object : XC_MethodHook() {
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param)
val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer")
val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java)
Log.i(TAG, "UnityPlayer.loadNative")
if (alreadyInitialized) {
return
}
val app = AndroidAppHelper.currentApplication()
if (nativeLibLoadSuccess) {
showToast("lib$nativeLibName.so loaded.")
}
else {
showToast("Load native library lib$nativeLibName.so failed.")
return
}
if (!gkmsDataInited) {
requestConfig(app.applicationContext)
}
FilesChecker.initDir(app.filesDir, modulePath)
initHook(
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
File(
app.filesDir.absolutePath,
FilesChecker.localizationFilesDir
).absolutePath
)
alreadyInitialized = true
}
})
hook(loadNativeMethod).intercept { chain ->
val result = chain.proceed()
onUnityLoadNativeAfterHook()
result
}
startLoop()
}
private fun hookMethod(
classLoader: ClassLoader,
className: String,
methodName: String,
parameterTypes: Array<Class<*>>,
before: ((XposedInterface.Chain) -> Unit)? = null,
after: ((XposedInterface.Chain, Any?) -> Unit)? = null,
) {
val clazz = classLoader.loadClass(className)
val method = clazz.getDeclaredMethod(methodName, *parameterTypes)
hook(method).intercept { chain ->
before?.invoke(chain)
val result = chain.proceed()
after?.invoke(chain, result)
result
}
}
private fun hookAllMethods(clazz: Class<*>, methodName: String, interceptor: (XposedInterface.Chain) -> Any?) {
val allMethods = (clazz.declaredMethods.asSequence() + clazz.methods.asSequence())
.filter { it.name == methodName }
.distinctBy(Method::toGenericString)
.toList()
allMethods.forEach { method ->
hook(method).intercept { chain -> interceptor(chain) }
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun onUnityLoadNativeAfterHook() {
Log.i(TAG, "UnityPlayer.loadNative")
if (alreadyInitialized) {
return
}
val app = getCurrentApplication()
if (app == null) {
Log.e(TAG, "currentApplication is null")
return
}
if (nativeLibLoadSuccess) {
showToast("lib$nativeLibName.so loaded.")
} else {
showToast("Load native library lib$nativeLibName.so failed.")
return
}
if (!gkmsDataInited) {
requestConfig(app.applicationContext)
}
FilesChecker.initDir(app.filesDir, modulePath)
initHook(
"${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
File(
app.filesDir.absolutePath,
FilesChecker.localizationFilesDir
).absolutePath
)
alreadyInitialized = true
}
private fun getCurrentApplication(): Application? {
return try {
val activityThreadClass = Class.forName("android.app.ActivityThread")
val method = activityThreadClass.getDeclaredMethod("currentApplication")
method.isAccessible = true
method.invoke(null) as? Application
} catch (_: Throwable) {
null
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun startLoop() {
GlobalScope.launch {
@ -231,8 +266,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
if (NativeInitProgress.startInit) {
initProgressUI.createView(gameActivity!!)
}
else {
} else {
initProgressUI.finishLoad(gameActivity!!)
}
}
@ -286,7 +320,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
// 使用热更新文件
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
// val dataUri = intent.data
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("resource_file", Uri::class.java)
} else {
@ -297,7 +330,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true
// Log.d(TAG, "dataUri: $dataUri")
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
programConfig.delRemoteAfterUpdate)
}
@ -444,10 +476,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
}
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
modulePath = startupParam.modulePath
}
companion object {
@JvmStatic
external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
@ -473,7 +501,15 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic
fun showToast(message: String) {
val app = AndroidAppHelper.currentApplication()
val app = try {
val activityThreadClass = Class.forName("android.app.ActivityThread")
val method = activityThreadClass.getDeclaredMethod("currentApplication")
method.isAccessible = true
method.invoke(null) as? Application
} catch (_: Throwable) {
null
}
val context = app?.applicationContext
if (context != null) {
val handler = Handler(Looper.getMainLooper())
@ -494,19 +530,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic
external fun pluginCallbackLooper(): Int
}
init {
ShadowHook.init(
ConfigBuilder()
.setMode(ShadowHook.Mode.UNIQUE)
.build()
)
nativeLibLoadSuccess = try {
System.loadLibrary(nativeLibName)
true
} catch (e: UnsatisfiedLinkError) {
false
}
}
}

View File

@ -48,8 +48,11 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
}
fun gotoPatchActivity() {
val intent = Intent(this, PatchActivity::class.java)
startActivity(intent)
mainUIConfirmStatUpdate(
isShow = true,
title = getString(R.string.patcher_unavailable_title),
content = getString(R.string.patcher_unavailable_content)
)
}
override fun saveConfig() {

View File

@ -1,15 +1,16 @@
package io.github.chinosk.gakumas.localify.hookUtils
import android.content.res.XModuleResources
import android.util.Log
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.util.zip.ZipFile
object FilesChecker {
private const val MODULE_ASSETS_PREFIX = "assets/"
lateinit var filesDir: File
lateinit var modulePath: String
val localizationFilesDir = "gakumas-local"
@ -17,7 +18,6 @@ object FilesChecker {
fun initAndCheck(fileDir: File, modulePath: String) {
initDir(fileDir, modulePath)
checkFiles()
}
@ -46,31 +46,28 @@ object FilesChecker {
pluginBasePath.mkdirs()
}
val assets = XModuleResources.createInstance(modulePath, null).assets
fun forAllAssetFiles(
basePath: String,
action: (String, InputStream?) -> Unit
) {
val assetFiles = assets.list(basePath)!!
for (file in assetFiles) {
try {
assets.open("$basePath/$file")
} catch (e: IOException) {
action("$basePath/$file", null)
forAllAssetFiles("$basePath/$file", action)
val rootAssetDir = moduleAssetPath(localizationFilesDir).trimEnd('/') + "/"
ZipFile(modulePath).use { zipFile ->
val entries = zipFile.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val name = entry.name
if (!name.startsWith(rootAssetDir)) continue
val relativePath = name.removePrefix(MODULE_ASSETS_PREFIX)
if (relativePath.isBlank()) continue
val outFile = File(filesDir, relativePath)
if (entry.isDirectory) {
outFile.mkdirs()
continue
}.use {
action("$basePath/$file", it)
}
}
}
forAllAssetFiles(localizationFilesDir) { path, file ->
val outFile = File(filesDir, path)
if (file == null) {
outFile.mkdirs()
} else {
outFile.outputStream().use { out ->
file.copyTo(out)
outFile.parentFile?.mkdirs()
zipFile.getInputStream(entry).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
@ -79,15 +76,13 @@ object FilesChecker {
}
fun getPluginVersion(): String {
val assets = XModuleResources.createInstance(modulePath, null).assets
for (i in assets.list(localizationFilesDir)!!) {
if (i.toString() == "version.txt") {
val stream = assets.open("$localizationFilesDir/$i")
val versionAssetPath = moduleAssetPath("$localizationFilesDir/version.txt")
ZipFile(modulePath).use { zipFile ->
val entry = zipFile.getEntry(versionAssetPath) ?: return "0.0"
zipFile.getInputStream(entry).use { stream ->
return convertToString(stream).trim()
}
}
return "0.0"
}
fun getInstalledVersion(): String {
@ -100,26 +95,25 @@ object FilesChecker {
}
fun convertToString(inputStream: InputStream?): String {
val stringBuilder = StringBuilder()
var reader: BufferedReader? = null
try {
reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
stringBuilder.append(line)
if (inputStream == null) return ""
return try {
BufferedReader(InputStreamReader(inputStream)).use { reader ->
buildString {
var line: String?
while (reader.readLine().also { line = it } != null) {
append(line)
}
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
""
}
return stringBuilder.toString()
}
private fun moduleAssetPath(path: String): String {
val cleanPath = path.trimStart('/')
return "$MODULE_ASSETS_PREFIX$cleanPath"
}
private fun deleteRecursively(file: File): Boolean {

View File

@ -299,6 +299,8 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。
個人データのバックアップを設定済みであることを確認してください。"</string>
<string name="patcher_unavailable_title">Patcher は利用できません</string>
<string name="patcher_unavailable_content">GKMSPatchを使用して、root権限なしでご利用ください</string>
<string name="path_password_eye">M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z</string>
<string name="path_password_eye_mask_strike_through">M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
<string name="path_password_eye_mask_visible">M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>

View File

@ -102,6 +102,8 @@
<string name="patch_uninstall_text">由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。</string>
<string name="patch_uninstall_confirm">您确定要卸载吗</string>
<string name="patch_finished">修补完成,是否开始安装?</string>
<string name="patcher_unavailable_title">Patcher 暂不可用</string>
<string name="patcher_unavailable_content">请使用GKMSPatch进行免Root使用</string>
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>

View File

@ -102,6 +102,8 @@
<string name="patch_uninstall_text">Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data.</string>
<string name="patch_uninstall_confirm">Are you sure you want to uninstall?</string>
<string name="patch_finished">Patch finished. Start installing?</string>
<string name="patcher_unavailable_title">Patcher Unavailable</string>
<string name="patcher_unavailable_content">Please use GKMSPatch to use this module rootlessly</string>
<string name="about_contributors_asset_file">about_contributors_en.json</string>
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>

View File

@ -0,0 +1 @@
io.github.chinosk.gakumas.localify.GakumasHookMain

View File

@ -0,0 +1,3 @@
minApiVersion=101
targetApiVersion=101
staticScope=true

View File

@ -0,0 +1 @@
com.bandainamcoent.idolmaster_gakuen

View File

@ -6,7 +6,7 @@ shizukuApi = "12.1.0"
hiddenapi-refine = "4.3.0"
hiddenapi-stub = "4.2.0"
okhttpBom = "4.12.0"
xposedApi = "82"
libxposedApi = "101.0.0"
appcompat = "1.7.0"
coil = "2.6.0"
composeBom = "2024.06.00"
@ -50,7 +50,7 @@ rikka-hidden-stub = { module = "dev.rikka.hidden:stub", version.ref = "hiddenapi
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposedApi" }
libxposed-api = { module = "io.github.libxposed:api", version.ref = "libxposedApi" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
material = { module = "com.google.android.material:material", version.ref = "material" }

View File

@ -18,7 +18,6 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url "https://api.xposed.info/" }
}
}