Compare commits

...

9 Commits
main ... main

18 changed files with 393 additions and 254 deletions

View File

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

View File

@ -130,6 +130,6 @@ dependencies {
implementation(libs.xdl) implementation(libs.xdl)
implementation(libs.shadowhook) implementation(libs.shadowhook)
compileOnly(libs.xposed.api) compileOnly(libs.libxposed.api)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
} }

View File

@ -21,23 +21,6 @@
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> 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 <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" 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 log
fmt) 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) target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_23)

View File

@ -1,11 +1,12 @@
#include "Misc.hpp" #include "Misc.hpp"
#include <codecvt>
#include <locale>
#include "fmt/core.h" #include "fmt/core.h"
#ifndef GKMS_WINDOWS #ifndef GKMS_WINDOWS
#include <jni.h> #include <jni.h>
#if defined(__ANDROID__) && __ANDROID_API__ >= 31
#include <unicode/ustring.h>
#endif
extern JavaVM* g_javaVM; extern JavaVM* g_javaVM;
#else #else
@ -32,14 +33,136 @@ namespace GakumasLocal::Misc {
return utility::conversions::utf16_to_utf8(wstr); return utility::conversions::utf16_to_utf8(wstr);
} }
#else #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::u16string ToUTF16(const std::string_view& str) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv; #if defined(__ANDROID__) && __ANDROID_API__ >= 31
return utf16conv.from_bytes(str.data(), str.data() + str.size()); 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::string ToUTF8(const std::u16string_view& str) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv; #if defined(__ANDROID__) && __ANDROID_API__ >= 31
return utf16conv.to_bytes(str.data(), str.data() + str.size()); 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 #endif

View File

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

View File

@ -3,7 +3,7 @@ package io.github.chinosk.gakumas.localify
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.app.AndroidAppHelper import android.app.Application
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@ -17,34 +17,32 @@ import android.view.MotionEvent
import android.widget.Toast import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import de.robv.android.xposed.IXposedHookLoadPackage import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
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.FilesChecker 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.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.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.lang.reflect.Method
import java.util.Locale import java.util.Locale
import kotlin.system.measureTimeMillis 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" val TAG = "GakumasLocalify"
class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { class GakumasHookMain : XposedModule() {
private lateinit var modulePath: String private var modulePath: String = ""
private var nativeLibLoadSuccess: Boolean private var nativeLibLoadSuccess: Boolean = false
private var alreadyInitialized = false private var alreadyInitialized = false
private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen" private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
private val nativeLibName = "MarryKotone" private val nativeLibName = "MarryKotone"
@ -55,49 +53,48 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var externalFilesChecked: Boolean = false private var externalFilesChecked: Boolean = false
private var gameActivity: Activity? = null private var gameActivity: Activity? = null
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
// if (lpparam.packageName == "io.github.chinosk.gakumas.localify") { modulePath = getModuleApplicationInfo().sourceDir
// 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}")
// }
// }
// )
// }
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 return
} }
XposedHelpers.findAndHookMethod( val classLoader = param.classLoader
"android.app.Activity",
lpparam.classLoader, hookMethod(
"dispatchKeyEvent", classLoader = classLoader,
KeyEvent::class.java, className = "android.app.Activity",
object : XC_MethodHook() { methodName = "dispatchKeyEvent",
override fun beforeHookedMethod(param: MethodHookParam) { parameterTypes = arrayOf(KeyEvent::class.java),
val keyEvent = param.args[0] as KeyEvent before = { chain ->
val keyCode = keyEvent.keyCode val keyEvent = chain.getArg(0) as KeyEvent
val action = keyEvent.action keyboardEvent(keyEvent.keyCode, keyEvent.action)
// Log.d(TAG, "Key event: keyCode=$keyCode, action=$action")
keyboardEvent(keyCode, action)
}
} }
) )
XposedHelpers.findAndHookMethod( hookMethod(
"android.app.Activity", classLoader = classLoader,
lpparam.classLoader, className = "android.app.Activity",
"dispatchGenericMotionEvent", methodName = "dispatchGenericMotionEvent",
MotionEvent::class.java, parameterTypes = arrayOf(MotionEvent::class.java),
object : XC_MethodHook() { before = { chain ->
override fun beforeHookedMethod(param: MethodHookParam) { val motionEvent = chain.getArg(0) as MotionEvent
val motionEvent = param.args[0] as MotionEvent
val action = motionEvent.action val action = motionEvent.action
// 左摇杆的X和Y轴 // 左摇杆的X和Y轴
@ -131,60 +128,91 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
hatY hatY
) )
} }
}
) )
val appActivityClass = XposedHelpers.findClass("android.app.Activity", lpparam.classLoader) val activityClass = classLoader.loadClass("android.app.Activity")
XposedBridge.hookAllMethods(appActivityClass, "onStart", object : XC_MethodHook() { hookAllMethods(activityClass, "onStart") { chain ->
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
Log.d(TAG, "onStart") Log.d(TAG, "onStart")
val currActivity = param.thisObject as Activity val currActivity = chain.thisObject as Activity
gameActivity = currActivity gameActivity = currActivity
if (getConfigError != null) { if (getConfigError != null) {
showGetConfigFailed(currActivity) showGetConfigFailed(currActivity)
} } else {
else {
initGkmsConfig(currActivity) initGkmsConfig(currActivity)
} }
chain.proceed()
} }
})
XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() { hookAllMethods(activityClass, "onResume") { chain ->
override fun beforeHookedMethod(param: MethodHookParam) {
Log.d(TAG, "onResume") Log.d(TAG, "onResume")
val currActivity = param.thisObject as Activity val currActivity = chain.thisObject as Activity
gameActivity = currActivity gameActivity = currActivity
if (getConfigError != null) { if (getConfigError != null) {
showGetConfigFailed(currActivity) showGetConfigFailed(currActivity)
} } else {
else {
initGkmsConfig(currActivity) initGkmsConfig(currActivity)
} }
chain.proceed()
}
val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer")
val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java)
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) }
}
} }
})
val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer")
XposedHelpers.findAndHookMethod(
cls,
"loadNative",
String::class.java,
object : XC_MethodHook() {
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
override fun afterHookedMethod(param: MethodHookParam) { private fun onUnityLoadNativeAfterHook() {
super.afterHookedMethod(param)
Log.i(TAG, "UnityPlayer.loadNative") Log.i(TAG, "UnityPlayer.loadNative")
if (alreadyInitialized) { if (alreadyInitialized) {
return return
} }
val app = AndroidAppHelper.currentApplication() val app = getCurrentApplication()
if (app == null) {
Log.e(TAG, "currentApplication is null")
return
}
if (nativeLibLoadSuccess) { if (nativeLibLoadSuccess) {
showToast("lib$nativeLibName.so loaded.") showToast("lib$nativeLibName.so loaded.")
} } else {
else {
showToast("Load native library lib$nativeLibName.so failed.") showToast("Load native library lib$nativeLibName.so failed.")
return return
} }
@ -204,9 +232,16 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
alreadyInitialized = true alreadyInitialized = true
} }
})
startLoop() 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) @OptIn(DelicateCoroutinesApi::class)
@ -231,8 +266,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
if (NativeInitProgress.startInit) { if (NativeInitProgress.startInit) {
initProgressUI.createView(gameActivity!!) initProgressUI.createView(gameActivity!!)
} } else {
else {
initProgressUI.finishLoad(gameActivity!!) initProgressUI.finishLoad(gameActivity!!)
} }
} }
@ -286,7 +320,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
// 使用热更新文件 // 使用热更新文件
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) { if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
// val dataUri = intent.data
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("resource_file", Uri::class.java) intent.getParcelableExtra("resource_file", Uri::class.java)
} else { } else {
@ -297,7 +330,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
if (dataUri != null) { if (dataUri != null) {
if (!externalFilesChecked) { if (!externalFilesChecked) {
externalFilesChecked = true externalFilesChecked = true
// Log.d(TAG, "dataUri: $dataUri")
FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir, FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
programConfig.delRemoteAfterUpdate) programConfig.delRemoteAfterUpdate)
} }
@ -444,10 +476,6 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
} }
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
modulePath = startupParam.modulePath
}
companion object { companion object {
@JvmStatic @JvmStatic
external fun initHook(targetLibraryPath: String, localizationFilesDir: String) external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
@ -473,7 +501,15 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic @JvmStatic
fun showToast(message: String) { 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 val context = app?.applicationContext
if (context != null) { if (context != null) {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -494,19 +530,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
@JvmStatic @JvmStatic
external fun pluginCallbackLooper(): Int 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() { fun gotoPatchActivity() {
val intent = Intent(this, PatchActivity::class.java) mainUIConfirmStatUpdate(
startActivity(intent) isShow = true,
title = getString(R.string.patcher_unavailable_title),
content = getString(R.string.patcher_unavailable_content)
)
} }
override fun saveConfig() { override fun saveConfig() {

View File

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

View File

@ -299,6 +299,8 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string> <string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。 <string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。
個人データのバックアップを設定済みであることを確認してください。"</string> 個人データのバックアップを設定済みであることを確認してください。"</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">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_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> <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_text">由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。</string>
<string name="patch_uninstall_confirm">您确定要卸载吗</string> <string name="patch_uninstall_confirm">您确定要卸载吗</string>
<string name="patch_finished">修补完成,是否开始安装?</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="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> <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_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_uninstall_confirm">Are you sure you want to uninstall?</string>
<string name="patch_finished">Patch finished. Start installing?</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="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> <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-refine = "4.3.0"
hiddenapi-stub = "4.2.0" hiddenapi-stub = "4.2.0"
okhttpBom = "4.12.0" okhttpBom = "4.12.0"
xposedApi = "82" libxposedApi = "101.0.0"
appcompat = "1.7.0" appcompat = "1.7.0"
coil = "2.6.0" coil = "2.6.0"
composeBom = "2024.06.00" 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" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp = { module = "com.squareup.okhttp3:okhttp" } okhttp = { module = "com.squareup.okhttp3:okhttp" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } 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-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
material = { module = "com.google.android.material:material", version.ref = "material" } material = { module = "com.google.android.material:material", version.ref = "material" }

View File

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