1
0
Fork 0

Compare commits

...

1 Commits

Author SHA1 Message Date
chinosk 6ddf4212d4
Add plugin initialization progress (#2)
* add plugin init progress

* add lazy initializing

* fix: lazy initializing - can't get class from `*` namespace

* fix: lazy mode - can't get class with empty namespace

* update build.yml - Adapt to Gitea Actions
2024-07-08 20:13:23 +08:00
14 changed files with 423 additions and 24 deletions

View File

@ -12,21 +12,22 @@ jobs:
with:
submodules: recursive
- name: set up android development environment
uses: android-actions/setup-android@v2
- name: install dependencies
run: |
sdkmanager --install "cmake;3.22.1"
echo "cmake.dir=/usr/local/lib/android/sdk/cmake/3.22.1" > local.properties
npm install -g pnpm
- name: Setup Java JDK
uses: actions/setup-java@v4.2.1
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Android Development Environment
uses: android-actions/setup-android@v3
- name: install dependencies
run: |
sdkmanager --install "cmake;3.22.1"
echo "cmake.dir=$ANDROID_HOME/cmake/3.22.1" > local.properties
echo "$ANDROID_HOME/build-tools/34.0.0" >> $GITHUB_PATH
npm install -g pnpm
- name: Update Submodules
run: |
git submodule foreach --recursive 'git pull --rebase origin main --allow-unrelated-histories'
@ -58,24 +59,44 @@ jobs:
run: ./gradlew build
- uses: actions/upload-artifact@v4
id: upload_unsigned_v4
with:
name: GakumasLocalify-Unsigned-apk
path: app/build/outputs/apk/debug/app-debug.apk
continue-on-error: true
- uses: ilharp/sign-android-release@v1
- name: Upload Unsigned APK with v3 if v4 failed
if: steps.upload_unsigned_v4.outcome == 'failure'
uses: actions/upload-artifact@v3
with:
name: GakumasLocalify-Unsigned-apk
path: app/build/outputs/apk/debug/app-debug.apk
continue-on-error: true
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
id: sign_app
with:
releaseDir: app/build/outputs/apk/debug
signingKey: ${{ secrets.KEYSTOREB64 }}
keyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
releaseDirectory: app/build/outputs/apk/debug
signingKeyBase64: ${{ secrets.KEYSTOREB64 }}
alias: ${{ secrets.ANDROID_KEY_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
buildToolsVersion: 33.0.0
env:
BUILD_TOOLS_VERSION: "34.0.0"
continue-on-error: true
- uses: actions/upload-artifact@v4
id: upload_signed_v4
with:
name: GakumasLocalify-Signed-apk
path: ${{steps.sign_app.outputs.signedFile}}
path: ${{steps.sign_app.outputs.signedReleaseFile}}
continue-on-error: true
- name: Upload Signed APK with v3 if v4 failed
if: steps.upload_signed_v4.outcome == 'failure'
uses: actions/upload-artifact@v3
with:
name: GakumasLocalify-Signed-apk
path: ${{steps.sign_app.outputs.signedReleaseFile}}
continue-on-error: true

View File

@ -39,7 +39,8 @@ std::unordered_set<void*> hookedStubs{};
GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr); \
} \
} \
else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr)
else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr); \
if (Config::lazyInit) UnityResolveProgress::classProgress.current++
void UnHookAll() {
for (const auto i: hookedStubs) {
@ -97,7 +98,7 @@ namespace GakumasLocal::HookMain {
UnityResolve::UnityType::Transform* cameraTransformCache = nullptr;
void CheckAndUpdateMainCamera() {
if (!Config::enableFreeCamera) return;
if (IsNativeObjectAlive(mainCameraCache)) return;
if (IsNativeObjectAlive(mainCameraCache) && IsNativeObjectAlive(cameraTransformCache)) return;
mainCameraCache = UnityResolve::UnityType::Camera::GetMain();
cameraTransformCache = mainCameraCache->GetTransform();
@ -827,7 +828,8 @@ namespace GakumasLocal::HookMain {
void StartInjectFunctions() {
const auto hookInstaller = Plugin::GetInstance().GetHookInstaller();
UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW), UnityResolve::Mode::Il2Cpp);
UnityResolve::Init(xdl_open(hookInstaller->m_il2cppLibraryPath.c_str(), RTLD_NOW),
UnityResolve::Mode::Il2Cpp, Config::lazyInit);
ADD_HOOK(AssetBundle_LoadAssetAsync, Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)"));
@ -959,10 +961,30 @@ namespace GakumasLocal::HookMain {
Log::Info("Start init plugin...");
if (Config::lazyInit) {
UnityResolveProgress::startInit = true;
UnityResolveProgress::assembliesProgress.total = 2;
UnityResolveProgress::assembliesProgress.current = 1;
UnityResolveProgress::classProgress.total = 36;
UnityResolveProgress::classProgress.current = 0;
}
StartInjectFunctions();
GKCamera::initCameraSettings();
if (Config::lazyInit) {
UnityResolveProgress::assembliesProgress.current = 2;
UnityResolveProgress::classProgress.total = 1;
UnityResolveProgress::classProgress.current = 0;
}
Local::LoadData();
if (Config::lazyInit) {
UnityResolveProgress::classProgress.current = 1;
UnityResolveProgress::startInit = false;
}
Log::Info("Plugin init finished.");
return ret;
}

View File

@ -7,6 +7,7 @@ namespace GakumasLocal::Config {
bool dbgMode = false;
bool enabled = true;
bool lazyInit = true;
bool replaceFont = true;
bool forceExportResource = true;
bool textTest = false;
@ -55,6 +56,7 @@ namespace GakumasLocal::Config {
GetConfigItem(dbgMode);
GetConfigItem(enabled);
GetConfigItem(lazyInit);
GetConfigItem(replaceFont);
GetConfigItem(forceExportResource);
GetConfigItem(gameOrientation);

View File

@ -5,6 +5,7 @@ namespace GakumasLocal::Config {
extern bool dbgMode;
extern bool enabled;
extern bool lazyInit;
extern bool replaceFont;
extern bool forceExportResource;
extern int gameOrientation;

View File

@ -47,6 +47,18 @@
#include "../../GakumasLocalify/Log.h"
#include "../../GakumasLocalify/Misc.hpp"
class UnityResolveProgress final {
public:
struct Progress {
long current = 0;
long total = 1;
};
static bool startInit;
static Progress assembliesProgress;
static Progress classProgress;
};
class UnityResolve final {
public:
struct Assembly;
@ -69,8 +81,16 @@ public:
[[nodiscard]] auto Get(const std::string& strClass, const std::string& strNamespace = "*", const std::string& strParent = "*") const -> Class* {
if (!this) return nullptr;
/*
if (lazyInit_ && classes.empty()) {
const auto image = Invoke<void*>("il2cpp_assembly_get_image", address);
ForeachClass(const_cast<Assembly *>(this), image);
}*/
for (const auto pClass : classes) if (strClass == pClass->name && (strNamespace == "*" || pClass->namespaze == strNamespace) && (strParent == "*" || pClass->parent == strParent)) return pClass;
return nullptr;
if (lazyInit_) {
return FillClass_Il2ccpp(const_cast<Assembly *>(this), strNamespace.c_str(), strClass.c_str());
}
return nullptr;
}
};
@ -279,14 +299,17 @@ public:
}
}
static auto Init(void* hmodule, const Mode mode = Mode::Mono) -> void {
static auto Init(void* hmodule, const Mode mode = Mode::Mono, const bool lazyInit = false) -> void {
mode_ = mode;
hmodule_ = hmodule;
lazyInit_ = lazyInit;
if (mode_ == Mode::Il2Cpp) {
if (!lazyInit) UnityResolveProgress::startInit = true;
pDomain = Invoke<void*>("il2cpp_domain_get");
Invoke<void*>("il2cpp_thread_attach", pDomain);
ForeachAssembly();
if (!lazyInit) UnityResolveProgress::startInit = false;
}
else {
pDomain = Invoke<void*>("mono_get_root_domain");
@ -561,7 +584,11 @@ private:
if (mode_ == Mode::Il2Cpp) {
size_t nrofassemblies = 0;
const auto assemblies = Invoke<void**>("il2cpp_domain_get_assemblies", pDomain, &nrofassemblies);
if (!lazyInit_) UnityResolveProgress::assembliesProgress.total = nrofassemblies;
for (auto i = 0; i < nrofassemblies; i++) {
if (!lazyInit_) UnityResolveProgress::assembliesProgress.current = i + 1;
const auto ptr = assemblies[i];
if (ptr == nullptr) continue;
auto assembly = new Assembly{ .address = ptr };
@ -569,7 +596,9 @@ private:
assembly->file = Invoke<const char*>("il2cpp_image_get_filename", image);
assembly->name = Invoke<const char*>("il2cpp_image_get_name", image);
UnityResolve::assembly.push_back(assembly);
ForeachClass(assembly, image);
if (!lazyInit_) {
ForeachClass(assembly, image);
}
}
}
else {
@ -590,11 +619,60 @@ private:
}
}
static auto GetPClassFromUnknownNamespace(void* image, const char* klassName) -> void* {
const auto count = Invoke<int>("il2cpp_image_get_class_count", image);
for (auto i = 0; i < count; i++) {
const auto pClass = Invoke<void*>("il2cpp_image_get_class", image, i);
const auto className = Invoke<const char*>("il2cpp_class_get_name", pClass);
if (strcmp(className, klassName) == 0) {
return pClass;
}
}
return nullptr;
}
static auto FillClass_Il2ccpp(Assembly* assembly, const char* namespaze, const char* klassName) -> Class* {
auto image = Invoke<void*>("il2cpp_assembly_get_image", assembly->address);
void* pClass;
if (strcmp(namespaze, "*") == 0) {
pClass = GetPClassFromUnknownNamespace(image, klassName);
}
else {
pClass = Invoke<void*>("il2cpp_class_from_name", image, namespaze, klassName);
}
if (!pClass && (strlen(namespaze) == 0)) {
pClass = GetPClassFromUnknownNamespace(image, klassName);
}
if (pClass == nullptr) return nullptr;
const auto pAClass = new Class();
pAClass->address = pClass;
pAClass->name = Invoke<const char*>("il2cpp_class_get_name", pClass);
if (const auto pPClass = Invoke<void*>("il2cpp_class_get_parent", pClass)) pAClass->parent = Invoke<const char*>("il2cpp_class_get_name", pPClass);
// pAClass->namespaze = Invoke<const char*>("il2cpp_class_get_namespace", pClass);
pAClass->namespaze = namespaze;
assembly->classes.push_back(pAClass);
ForeachFields(pAClass, pClass);
ForeachMethod(pAClass, pClass);
void* i_class{};
void* iter{};
do {
if ((i_class = Invoke<void*>("il2cpp_class_get_interfaces", pClass, &iter))) {
ForeachFields(pAClass, i_class);
ForeachMethod(pAClass, i_class);
}
} while (i_class);
return pAClass;
}
static auto ForeachClass(Assembly* assembly, void* image) -> void {
// 遍历类
if (mode_ == Mode::Il2Cpp) {
const auto count = Invoke<int>("il2cpp_image_get_class_count", image);
if (!lazyInit_) UnityResolveProgress::classProgress.total = count;
for (auto i = 0; i < count; i++) {
if (!lazyInit_) UnityResolveProgress::classProgress.current = i + 1;
const auto pClass = Invoke<void*>("il2cpp_image_get_class", image, i);
if (pClass == nullptr) continue;
const auto pAClass = new Class();
@ -2586,6 +2664,7 @@ public:
private:
inline static Mode mode_{};
inline static void* hmodule_;
inline static bool lazyInit_;
inline static std::unordered_map<std::string, void*> address_{};
inline static void* pDomain{};
};

View File

@ -15,6 +15,10 @@ JavaVM* g_javaVM = nullptr;
jclass g_gakumasHookMainClass = nullptr;
jmethodID showToastMethodId = nullptr;
bool UnityResolveProgress::startInit = false;
UnityResolveProgress::Progress UnityResolveProgress::assembliesProgress{};
UnityResolveProgress::Progress UnityResolveProgress::classProgress{};
namespace
{
class AndroidHookInstaller : public GakumasLocal::HookInstaller
@ -114,8 +118,37 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env,
}
extern "C"
JNIEXPORT void JNICALL
JNIEXPORT jint JNICALL
Java_io_github_chinosk_gakumas_localify_GakumasHookMain_pluginCallbackLooper(JNIEnv *env,
jclass clazz) {
GakumasLocal::Log::ToastLoop(env, clazz);
if (UnityResolveProgress::startInit) {
return 9;
}
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_io_github_chinosk_gakumas_localify_models_NativeInitProgress_pluginInitProgressLooper(
JNIEnv *env, jclass clazz, jobject progress) {
// jclass progressClass = env->GetObjectClass(progress);
static jfieldID startInitFieldID = env->GetStaticFieldID(clazz, "startInit", "Z");
static jmethodID setAssembliesProgressDataMethodID = env->GetMethodID(clazz, "setAssembliesProgressData", "(JJ)V");
static jmethodID setClassProgressDataMethodID = env->GetMethodID(clazz, "setClassProgressData", "(JJ)V");
// jboolean startInit = env->GetStaticBooleanField(clazz, startInitFieldID);
env->SetStaticBooleanField(clazz, startInitFieldID, UnityResolveProgress::startInit);
env->CallVoidMethod(progress, setAssembliesProgressDataMethodID,
UnityResolveProgress::assembliesProgress.current, UnityResolveProgress::assembliesProgress.total);
env->CallVoidMethod(progress, setClassProgressDataMethodID,
UnityResolveProgress::classProgress.current, UnityResolveProgress::classProgress.total);
}

View File

@ -17,6 +17,7 @@ interface ConfigListener {
fun onForceExportResourceChanged(value: Boolean)
fun onTextTestChanged(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)
@ -111,6 +112,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onLazyInitChanged(value: Boolean) {
config.lazyInit = value
saveConfig()
}
override fun onTextTestChanged(value: Boolean) {
config.textTest = value
saveConfig()

View File

@ -34,7 +34,9 @@ import java.util.Locale
import kotlin.system.measureTimeMillis
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
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"
@ -49,6 +51,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var getConfigError: Exception? = null
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") {
@ -135,6 +138,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
super.beforeHookedMethod(param)
Log.d(TAG, "onStart")
val currActivity = param.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
}
@ -148,6 +152,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
override fun beforeHookedMethod(param: MethodHookParam) {
Log.d(TAG, "onResume")
val currActivity = param.thisObject as Activity
gameActivity = currActivity
if (getConfigError != null) {
showGetConfigFailed(currActivity)
}
@ -206,9 +211,30 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private fun startLoop() {
GlobalScope.launch {
val interval = 1000L / 30
var lastFrameStartInit = NativeInitProgress.startInit
val initProgressUI = InitProgressUI()
while (isActive) {
val timeTaken = measureTimeMillis {
pluginCallbackLooper()
val returnValue = pluginCallbackLooper() // plugin main thread loop
if (returnValue == 9) {
NativeInitProgress.startInit = true
}
if (NativeInitProgress.startInit) { // if init, update data
NativeInitProgress.pluginInitProgressLooper(NativeInitProgress)
gameActivity?.let { initProgressUI.updateData(it) }
}
if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) { // change status
if (NativeInitProgress.startInit) {
initProgressUI.createView(gameActivity!!)
}
else {
initProgressUI.finishLoad(gameActivity!!)
}
}
lastFrameStartInit = NativeInitProgress.startInit
}
delay(interval - timeTaken)
}
@ -413,7 +439,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
}
@JvmStatic
external fun pluginCallbackLooper()
external fun pluginCallbackLooper(): Int
}
init {

View File

@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
data class GakumasConfig (
var dbgMode: Boolean = false,
var enabled: Boolean = true,
var lazyInit: Boolean = true,
var replaceFont: Boolean = true,
var textTest: Boolean = false,
var dumpText: Boolean = false,

View File

@ -0,0 +1,25 @@
package io.github.chinosk.gakumas.localify.models
data class ProgressData(
var current: Long = 0,
var total: Long = 1
)
object NativeInitProgress {
var assembliesProgress = ProgressData()
var classProgress = ProgressData()
var startInit: Boolean = false
fun setAssembliesProgressData(current: Long, total: Long) {
assembliesProgress.current = current
assembliesProgress.total = total
}
fun setClassProgressData(current: Long, total: Long) {
classProgress.current = current
classProgress.total = total
}
@JvmStatic
external fun pluginInitProgressLooper(progress: NativeInitProgress)
}

View File

@ -0,0 +1,176 @@
package io.github.chinosk.gakumas.localify.ui.game_attach
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.util.Log
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import io.github.chinosk.gakumas.localify.TAG
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class InitProgressUI {
private var uiCreated = false
private lateinit var rootView: ViewGroup
private lateinit var container: LinearLayout
private lateinit var assembliesProgressBar: ProgressBar
private lateinit var classProgressBar: ProgressBar
private lateinit var titleText: TextView
private lateinit var assembliesProgressText: TextView
private lateinit var classProgressText: TextView
@SuppressLint("SetTextI18n")
fun createView(context: Context) {
if (uiCreated) return
uiCreated = true
val activity = context as? Activity ?: return
rootView = activity.findViewById<ViewGroup>(android.R.id.content)
container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = Gravity.TOP or Gravity.END
marginEnd = 20
marginStart = 20
topMargin = 100
}
setBackgroundColor(Color.WHITE)
setPadding(20, 20, 20, 20)
}
// Set up the container layout
assembliesProgressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = 20
}
max = 100
}
// Set up the class progress bar
classProgressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = 20
}
max = 100
}
assembliesProgressBar.progressTintList = ColorStateList.valueOf(Color.parseColor("#FFF89400"))
classProgressBar.progressTintList = ColorStateList.valueOf(Color.parseColor("#FFF89400"))
// Set up the text views
titleText = TextView(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = 20
gravity = Gravity.CENTER_HORIZONTAL
}
setTextColor(Color.BLACK)
text = "Initializing"
textSize = 20f
setTypeface(typeface, Typeface.BOLD)
}
val textLayout = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = 20
}
assembliesProgressText = TextView(context).apply {
layoutParams = textLayout
setTextColor(Color.BLACK)
}
classProgressText = TextView(context).apply {
layoutParams = textLayout
setTextColor(Color.BLACK)
}
// Add container to the root view
context.runOnUiThread {
// Add views to the container
container.addView(titleText)
container.addView(assembliesProgressText)
container.addView(assembliesProgressBar)
container.addView(classProgressText)
container.addView(classProgressBar)
rootView.addView(container)
}
}
@SuppressLint("SetTextI18n")
@OptIn(DelicateCoroutinesApi::class)
fun finishLoad(context: Activity) {
if (!uiCreated) return
uiCreated = false
GlobalScope.launch {
context.runOnUiThread {
assembliesProgressBar.progressTintList = ColorStateList.valueOf(Color.parseColor("#FF28B463"))
classProgressBar.progressTintList = ColorStateList.valueOf(Color.parseColor("#FF28B463"))
titleText.text = "Finished"
}
delay(1500L)
context.runOnUiThread {
rootView.removeView(container)
}
}
}
fun removeView(context: Activity) {
if (!uiCreated) return
uiCreated = false
context.runOnUiThread {
rootView.removeView(container)
}
}
@SuppressLint("SetTextI18n")
fun updateData(context: Activity) {
if (!uiCreated) return
//return
context.runOnUiThread {
val assembliesProgress = NativeInitProgress.assembliesProgress
val classProgress = NativeInitProgress.classProgress
assembliesProgressText.text = "${assembliesProgress.current}/${assembliesProgress.total}"
classProgressText.text = "${classProgress.current}/${classProgress.total}"
assembliesProgressBar.setProgress((assembliesProgress.current * 100 / assembliesProgress.total).toInt(), true)
classProgressBar.setProgress((classProgress.current * 100 / classProgress.total).toInt(), true)
}
}
}

View File

@ -141,9 +141,14 @@ fun HomePage(modifier: Modifier = Modifier,
v -> context?.onEnabledChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.lazy_init), checked = config.value.lazyInit) {
v -> context?.onLazyInitChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.replace_font), checked = config.value.replaceFont) {
v -> context?.onReplaceFontChanged(v)
}
}
}
Spacer(Modifier.height(6.dp))

View File

@ -3,6 +3,7 @@
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">启用插件 (不可热重载)</string>
<string name="replace_font">替换字体</string>
<string name="lazy_init">快速初始化(懒加载配置)</string>
<string name="enable_free_camera">启用自由视角(可热重载; 需使用实体键盘)</string>
<string name="start_game">以上述配置启动游戏/重载配置</string>
<string name="setFpsTitle">最大 FPS (0 为保持游戏原设置)</string>

View File

@ -3,6 +3,7 @@
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">Enable Plugin (Not Hot Reloadable)</string>
<string name="replace_font">Replace Font</string>
<string name="lazy_init">Fast Initialization (Lazy loading)</string>
<string name="enable_free_camera">Enable Free Camera</string>
<string name="start_game">Start Game / Hot Reload Config</string>
<string name="setFpsTitle">Max FPS (0 is Use Original Settings)</string>