diff --git a/app/build.gradle b/app/build.gradle index a757d4c..554a761 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,6 +66,9 @@ android { pickFirst '**/libxdl.so' pickFirst '**/libshadowhook.so' } + dataBinding { + enable true + } } dependencies { @@ -78,6 +81,9 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -89,4 +95,6 @@ dependencies { implementation 'io.github.hexhacking:xdl:2.1.1' implementation 'com.bytedance.android:shadowhook:1.0.9' compileOnly 'de.robv.android.xposed:api:82' + implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.0" + implementation 'com.google.code.gson:gson:2.11.0' } \ No newline at end of file diff --git a/app/src/main/1024.png b/app/src/main/1024.png new file mode 100644 index 0000000..345099b Binary files /dev/null and b/app/src/main/1024.png differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a578608..35891d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.GakumasLocalify" tools:targetApi="31"> @@ -16,20 +15,16 @@ - - - - +#include "camera/camera.hpp" +#include "config/Config.hpp" +#include "shadowhook.h" +#include +#include +std::unordered_set hookedStubs{}; + #define DEFINE_HOOK(returnType, name, params) \ using name##_Type = returnType(*) params; \ name##_Type name##_Addr = nullptr; \ @@ -18,13 +25,32 @@ #define ADD_HOOK(name, addr) \ name##_Addr = reinterpret_cast(addr); \ if (addr) { \ - hookInstaller->InstallHook(reinterpret_cast(addr), \ - reinterpret_cast(name##_Hook), \ - reinterpret_cast(&name##_Orig)); \ - GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr); \ + auto stub = hookInstaller->InstallHook(reinterpret_cast(addr), \ + reinterpret_cast(name##_Hook), \ + reinterpret_cast(&name##_Orig)); \ + if (stub == NULL) { \ + int error_num = shadowhook_get_errno(); \ + const char *error_msg = shadowhook_to_errmsg(error_num); \ + Log::ErrorFmt("ADD_HOOK: %s at %p failed: %s", #name, addr, error_msg); \ + } \ + else { \ + hookedStubs.emplace(stub); \ + GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr); \ + } \ } \ else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr) +void UnHookAll() { + for (const auto i: hookedStubs) { + int result = shadowhook_unhook(i); + if(result != 0) + { + int error_num = shadowhook_get_errno(); + const char *error_msg = shadowhook_to_errmsg(error_num); + GakumasLocal::Log::ErrorFmt("unhook failed: %d - %s", error_num, error_msg); + } + } +} namespace GakumasLocal::HookMain { using Il2cppString = UnityResolve::UnityType::String; @@ -58,6 +84,52 @@ namespace GakumasLocal::HookMain { // Log::LogFmt(ANDROID_LOG_VERBOSE, "UnityLog - Internal_Log: %s", content->ToString().c_str()); } + UnityResolve::UnityType::Camera* mainCameraCache = nullptr; + UnityResolve::UnityType::Transform* cameraTransformCache = nullptr; + void CheckAndUpdateMainCamera() { + if (!Config::enableFreeCamera) return; + static auto IsNativeObjectAlive = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine", + "Object", "IsNativeObjectAlive"); + + if (IsNativeObjectAlive->Invoke(mainCameraCache)) return; + + mainCameraCache = UnityResolve::UnityType::Camera::GetMain(); + cameraTransformCache = mainCameraCache->GetTransform(); + } + + DEFINE_HOOK(void, Unity_set_position_Injected, (UnityResolve::UnityType::Transform* _this, UnityResolve::UnityType::Vector3* data)) { + if (Config::enableFreeCamera) { + CheckAndUpdateMainCamera(); + + if (cameraTransformCache == _this) { + //Log::DebugFmt("MainCamera set pos: %f, %f, %f", data->x, data->y, data->z); + auto& origCameraPos = GKCamera::baseCamera.pos; + data->x = origCameraPos.x; + data->y = origCameraPos.y; + data->z = origCameraPos.z; + } + } + + return Unity_set_position_Injected_Orig(_this, data); + } + + DEFINE_HOOK(void, Unity_set_rotation_Injected, (UnityResolve::UnityType::Transform* _this, UnityResolve::UnityType::Quaternion* value)) { + if (Config::enableFreeCamera) { + if (cameraTransformCache == _this) { + auto& origCameraLookat = GKCamera::baseCamera.lookAt; + static auto lookat_injected = reinterpret_cast( + Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.Transform::Internal_LookAt_Injected(UnityEngine.Vector3&,UnityEngine.Vector3&)")); + static auto worldUp = UnityResolve::UnityType::Vector3(0, 1, 0); + lookat_injected(_this, &origCameraLookat, &worldUp); + // TODO 相机 FOV + return; + } + } + return Unity_set_rotation_Injected_Orig(_this, value); + } + std::unordered_map loadHistory{}; DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* _this, Il2cppString* name, void* type)) { @@ -192,14 +264,10 @@ namespace GakumasLocal::HookMain { TextMeshProUGUI_Awake_Orig(_this, method); } - DEFINE_HOOK(void, UI_Text_set_text, (void* _this, Il2cppString* value)) { - // UI_Text_set_text_Orig(_this, Il2cppString::New("[US]" + value->ToString())); - UI_Text_set_text_Orig(_this, value); - - static auto set_font = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", - "TMP_Text", "set_font"); - auto newFont = GetReplaceFont(); - set_font->Invoke(_this, newFont); + // TODO 文本未hook完整 思路:从tips下手... + DEFINE_HOOK(void, TextField_set_value, (void* _this, Il2cppString* value)) { + Log::DebugFmt("TextField_set_value: %s", value->ToString().c_str()); + TextField_set_value_Orig(_this, value); } DEFINE_HOOK(Il2cppString*, OctoCaching_GetResourceFileName, (void* data, void* method)) { @@ -273,8 +341,8 @@ namespace GakumasLocal::HookMain { ADD_HOOK(TMP_Text_set_text, Il2cppUtils::GetMethodPointer("Unity.TextMeshPro.dll", "TMPro", "TMP_Text", "set_text")); - ADD_HOOK(UI_Text_set_text, Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI", - "Text", "set_text")); + ADD_HOOK(TextField_set_value, Il2cppUtils::GetMethodPointer("UnityEngine.UIElementsModule.dll", "UnityEngine.UIElements", + "TextField", "set_value")); ADD_HOOK(OctoCaching_GetResourceFileName, Il2cppUtils::GetMethodPointer("Octo.dll", "Octo.Caching", "OctoCaching", "GetResourceFileName")); @@ -292,6 +360,11 @@ namespace GakumasLocal::HookMain { "UnityEngine.DebugLogHandler::Internal_LogException(System.Exception,UnityEngine.Object)")); ADD_HOOK(Internal_Log, Il2cppUtils::il2cpp_resolve_icall( "UnityEngine.DebugLogHandler::Internal_Log(UnityEngine.LogType,UnityEngine.LogOption,System.String,UnityEngine.Object)")); + + ADD_HOOK(Unity_set_position_Injected, Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.Transform::set_position_Injected(UnityEngine.Vector3&)")); + ADD_HOOK(Unity_set_rotation_Injected, Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.Transform::set_rotation_Injected(UnityEngine.Quaternion&)")); } // 77 2640 5000 @@ -299,9 +372,20 @@ namespace GakumasLocal::HookMain { const auto ret = il2cpp_init_Orig(domain_name); // InjectFunctions(); + Log::Info("Waiting for config..."); + + while (!Config::isConfigInit) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (!Config::enabled) { + Log::Info("Plugin not enabled"); + return ret; + } + Log::Info("Start init plugin..."); StartInjectFunctions(); + GKCamera::initCameraSettings(); Local::LoadData(); Log::Info("Plugin init finished."); @@ -321,4 +405,4 @@ namespace GakumasLocal::Hook { Log::Info("Hook installed"); } -} \ No newline at end of file +} diff --git a/app/src/main/cpp/GakumasLocalify/Plugin.h b/app/src/main/cpp/GakumasLocalify/Plugin.h index 1ac6dab..bba25f1 100644 --- a/app/src/main/cpp/GakumasLocalify/Plugin.h +++ b/app/src/main/cpp/GakumasLocalify/Plugin.h @@ -9,7 +9,7 @@ namespace GakumasLocal { struct HookInstaller { virtual ~HookInstaller(); - virtual void InstallHook(void* addr, void* hook, void** orig) = 0; + virtual void* InstallHook(void* addr, void* hook, void** orig) = 0; virtual OpaqueFunctionPointer LookupSymbol(const char* name) = 0; std::string m_il2cppLibraryPath; diff --git a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp new file mode 100644 index 0000000..0c7b837 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.cpp @@ -0,0 +1,125 @@ +#include "baseCamera.hpp" +#include + + +namespace BaseCamera { + using Vector3_t = UnityResolve::UnityType::Vector3; + + float moveStep = 0.05; + float look_radius = 5; // 转向半径 + float moveAngel = 1.5; // 转向角度 + + int smoothLevel = 1; + unsigned long sleepTime = 0; + + + Camera::Camera() { + Camera(0, 0, 0, 0, 0, 0); + } + + Camera::Camera(Vector3_t* vec, Vector3_t* lookAt) { + Camera(vec->x, vec->y, vec->z, lookAt->x, lookAt->y, lookAt->z); + } + + Camera::Camera(Vector3_t& vec, Vector3_t& lookAt) { + Camera(vec.x, vec.y, vec.z, lookAt.x, lookAt.y, lookAt.z); + } + + Camera::Camera(float x, float y, float z, float lx, float ly, float lz) { + pos.x = x; + pos.y = y; + pos.z = z; + lookAt.x = lx; + lookAt.y = ly; + lookAt.z = lz; + } + + void Camera::setPos(float x, float y, float z) { + pos.x = x; + pos.y = y; + pos.z = z; + } + + void Camera::setLookAt(float x, float y, float z) { + lookAt.x = x; + lookAt.y = y; + lookAt.z = z; + } + + void Camera::reset() { + setPos(0.5, 1.1, 1.3); + setLookAt(0.5, 1.1, -3.7); + fov = 60; + verticalAngle = 0; + horizontalAngle = 0; + } + + Vector3_t Camera::GetPos() { + return pos; + } + + Vector3_t Camera::GetLookAt() { + return lookAt; + } + + void Camera::set_lon_move(float vertanglePlus, LonMoveHState moveState) { // 前后移动 + auto radian = (verticalAngle + vertanglePlus) * M_PI / 180; + auto radianH = (double)horizontalAngle * M_PI / 180; + + auto f_step = cos(radian) * moveStep * cos(radianH) / smoothLevel; // ↑↓ + auto l_step = sin(radian) * moveStep * cos(radianH) / smoothLevel; // ←→ + // auto h_step = tan(radianH) * sqrt(pow(f_step, 2) + pow(l_step, 2)); + auto h_step = sin(radianH) * moveStep / smoothLevel; + + switch (moveState) + { + case LonMoveForward: break; + case LonMoveBack: h_step = -h_step; break; + default: h_step = 0; break; + } + + for (int i = 0; i < smoothLevel; i++) { + pos.z -= f_step; + lookAt.z -= f_step; + pos.x += l_step; + lookAt.x += l_step; + pos.y += h_step; + lookAt.y += h_step; + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime)); + } + } + + void Camera::updateVertLook() { // 上+ + auto radian = verticalAngle * M_PI / 180; + auto radian2 = ((double)horizontalAngle - 90) * M_PI / 180; // 日 + + auto stepX1 = look_radius * sin(radian2) * cos(radian) / smoothLevel; + auto stepX2 = look_radius * sin(radian2) * sin(radian) / smoothLevel; + auto stepX3 = look_radius * cos(radian2) / smoothLevel; + + for (int i = 0; i < smoothLevel; i++) { + lookAt.z = pos.z + stepX1; + lookAt.y = pos.y + stepX3; + lookAt.x = pos.x - stepX2; + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime)); + } + } + + void Camera::setHoriLook(float vertangle) { // 左+ + auto radian = vertangle * M_PI / 180; + auto radian2 = horizontalAngle * M_PI / 180; + + auto stepBt = cos(radian) * look_radius * cos(radian2) / smoothLevel; + auto stepHi = sin(radian) * look_radius * cos(radian2) / smoothLevel; + auto stepY = sin(radian2) * look_radius / smoothLevel; + + for (int i = 0; i < smoothLevel; i++) { + lookAt.x = pos.x + stepHi; + lookAt.z = pos.z - stepBt; + lookAt.y = pos.y + stepY; + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime)); + } + } + + +} diff --git a/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp new file mode 100644 index 0000000..20051b2 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/camera/baseCamera.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "../deps/UnityResolve/UnityResolve.hpp" + +enum LonMoveHState { + LonMoveLeftAndRight, + LonMoveForward, + LonMoveBack +}; + +namespace BaseCamera { + using Vector3_t = UnityResolve::UnityType::Vector3; + + extern float moveStep; + extern float look_radius; // 转向半径 + extern float moveAngel; // 转向角度 + + extern int smoothLevel; + extern unsigned long sleepTime; + + + class Camera { + public: + Camera(); + Camera(Vector3_t& vec, Vector3_t& lookAt); + Camera(Vector3_t* vec, Vector3_t* lookAt); + Camera(float x, float y, float z, float lx, float ly, float lz); + + void reset(); + void setPos(float x, float y, float z); + void setLookAt(float x, float y, float z); + + void set_lon_move(float vertanglePlus, LonMoveHState moveState = LonMoveHState::LonMoveLeftAndRight); + void updateVertLook(); + void setHoriLook(float vertangle); + + Vector3_t GetPos(); + Vector3_t GetLookAt(); + + Vector3_t pos{0.5, 1.1, 1.3}; + Vector3_t lookAt{0.5, 1.1, -3.7}; + float fov = 60; + + float horizontalAngle = 0; // 水平方向角度 + float verticalAngle = 0; // 垂直方向角度 + + }; + +} diff --git a/app/src/main/cpp/GakumasLocalify/camera/camera.cpp b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp new file mode 100644 index 0000000..72491cc --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp @@ -0,0 +1,197 @@ +#include "baseCamera.hpp" +#include + +#define KEY_W 51 +#define KEY_S 47 +#define KEY_A 29 +#define KEY_D 32 +#define KEY_R 46 +#define KEY_Q 45 +#define KEY_E 33 +#define KEY_I 37 +#define KEY_K 39 +#define KEY_J 38 +#define KEY_L 40 +#define KEY_R 46 +#define KEY_UP 19 +#define KEY_DOWN 20 +#define KEY_LEFT 21 +#define KEY_RIGHT 22 +#define KEY_CTRL 113 +#define KEY_SHIFT 59 +#define KEY_ALT 57 +#define KEY_SPACE 62 + +#define WM_KEYDOWN 0 +#define WM_KEYUP 1 + + +namespace GKCamera { + BaseCamera::Camera baseCamera{}; + + bool rMousePressFlg = false; + + void reset_camera() { + baseCamera.reset(); + } + + void camera_forward() { // 向前 + baseCamera.set_lon_move(0, LonMoveHState::LonMoveForward); + } + void camera_back() { // 后退 + baseCamera.set_lon_move(180, LonMoveHState::LonMoveBack); + } + void camera_left() { // 向左 + baseCamera.set_lon_move(90); + } + void camera_right() { // 向右 + baseCamera.set_lon_move(-90); + } + void camera_down() { // 向下 + float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel; + + for (int i = 0; i < BaseCamera::smoothLevel; i++) { + baseCamera.pos.y -= preStep; + baseCamera.lookAt.y -= preStep; + std::this_thread::sleep_for(std::chrono::milliseconds(BaseCamera::sleepTime)); + } + } + void camera_up() { // 向上 + float preStep = BaseCamera::moveStep / BaseCamera::smoothLevel; + + for (int i = 0; i < BaseCamera::smoothLevel; i++) { + baseCamera.pos.y += preStep; + baseCamera.lookAt.y += preStep; + std::this_thread::sleep_for(std::chrono::milliseconds(BaseCamera::sleepTime)); + } + } + void cameraLookat_up(float mAngel, bool mouse = false) { + baseCamera.horizontalAngle += mAngel; + if (baseCamera.horizontalAngle >= 90) baseCamera.horizontalAngle = 89.99; + baseCamera.updateVertLook(); + } + void cameraLookat_down(float mAngel, bool mouse = false) { + baseCamera.horizontalAngle -= mAngel; + if (baseCamera.horizontalAngle <= -90) baseCamera.horizontalAngle = -89.99; + baseCamera.updateVertLook(); + } + void cameraLookat_left(float mAngel) { + baseCamera.verticalAngle += mAngel; + if (baseCamera.verticalAngle >= 360) baseCamera.verticalAngle = -360; + baseCamera.setHoriLook(baseCamera.verticalAngle); + } + void cameraLookat_right(float mAngel) { + baseCamera.verticalAngle -= mAngel; + if (baseCamera.verticalAngle <= -360) baseCamera.verticalAngle = 360; + baseCamera.setHoriLook(baseCamera.verticalAngle); + } + void changeCameraFOV(float value) { + baseCamera.fov += value; + } + + struct CameraMoveState { + bool w = false; + bool s = false; + bool a = false; + bool d = false; + bool ctrl = false; + bool space = false; + bool up = false; + bool down = false; + bool left = false; + bool right = false; + bool q = false; + bool e = false; + bool i = false; + bool k = false; + bool j = false; + bool l = false; + bool threadRunning = false; + + void resetAll() { + auto p = reinterpret_cast(this); + const auto numMembers = sizeof(*this) / sizeof(bool); + for (size_t idx = 0; idx < numMembers; ++idx) { + p[idx] = false; + } + } + } cameraMoveState; + + + void cameraRawInputThread() { + using namespace BaseCamera; + + std::thread([]() { + if (cameraMoveState.threadRunning) return; + cameraMoveState.threadRunning = true; + while (true) { + if (cameraMoveState.w) camera_forward(); + if (cameraMoveState.s) camera_back(); + if (cameraMoveState.a) camera_left(); + if (cameraMoveState.d) camera_right(); + if (cameraMoveState.ctrl) camera_down(); + if (cameraMoveState.space) camera_up(); + if (cameraMoveState.up) cameraLookat_up(moveAngel); + if (cameraMoveState.down) cameraLookat_down(moveAngel); + if (cameraMoveState.left) cameraLookat_left(moveAngel); + if (cameraMoveState.right) cameraLookat_right(moveAngel); + if (cameraMoveState.q) changeCameraFOV(0.5f); + if (cameraMoveState.e) changeCameraFOV(-0.5f); + // if (cameraMoveState.i) changeLiveFollowCameraOffsetY(moveStep / 3); + // if (cameraMoveState.k) changeLiveFollowCameraOffsetY(-moveStep / 3); + // if (cameraMoveState.j) changeLiveFollowCameraOffsetX(moveStep * 10); + // if (cameraMoveState.l) changeLiveFollowCameraOffsetX(-moveStep * 10); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }).detach(); + } + + void on_cam_rawinput_keyboard(int message, int key) { + if (message == WM_KEYDOWN || message == WM_KEYUP) { + switch (key) { + case KEY_W: + cameraMoveState.w = message == WM_KEYDOWN; break; + case KEY_S: + cameraMoveState.s = message == WM_KEYDOWN; break; + case KEY_A: + cameraMoveState.a = message == WM_KEYDOWN; break; + case KEY_D: + cameraMoveState.d = message == WM_KEYDOWN; break; + case KEY_CTRL: + cameraMoveState.ctrl = message == WM_KEYDOWN; break; + case KEY_SPACE: + cameraMoveState.space = message == WM_KEYDOWN; break; + case KEY_UP: + cameraMoveState.up = message == WM_KEYDOWN; break; + case KEY_DOWN: + cameraMoveState.down = message == WM_KEYDOWN; break; + case KEY_LEFT: + cameraMoveState.left = message == WM_KEYDOWN; break; + case KEY_RIGHT: + cameraMoveState.right = message == WM_KEYDOWN; break; + case KEY_Q: + cameraMoveState.q = message == WM_KEYDOWN; break; + case KEY_E: + cameraMoveState.e = message == WM_KEYDOWN; break; + case KEY_I: + cameraMoveState.i = message == WM_KEYDOWN; break; + case KEY_K: + cameraMoveState.k = message == WM_KEYDOWN; break; + case KEY_J: + cameraMoveState.j = message == WM_KEYDOWN; break; + case KEY_L: + cameraMoveState.l = message == WM_KEYDOWN; break; + case KEY_R: { + if (message == WM_KEYDOWN) reset_camera(); + }; break; + default: break; + } + } + } + + void initCameraSettings() { + reset_camera(); + cameraRawInputThread(); + } + +} diff --git a/app/src/main/cpp/GakumasLocalify/camera/camera.hpp b/app/src/main/cpp/GakumasLocalify/camera/camera.hpp new file mode 100644 index 0000000..0be5fcd --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/camera/camera.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "baseCamera.hpp" + +namespace GKCamera { + extern BaseCamera::Camera baseCamera; + + void on_cam_rawinput_keyboard(int message, int key); + void initCameraSettings(); +} diff --git a/app/src/main/cpp/GakumasLocalify/config/Config.cpp b/app/src/main/cpp/GakumasLocalify/config/Config.cpp new file mode 100644 index 0000000..0f6aa71 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/config/Config.cpp @@ -0,0 +1,24 @@ +#include +#include "nlohmann/json.hpp" +#include "../Log.h" + +namespace GakumasLocal::Config { + bool isConfigInit = false; + + bool enabled = true; + bool enableFreeCamera = false; + + void LoadConfig(const std::string& configStr) { + try { + const auto config = nlohmann::json::parse(configStr); + + enabled = config["enabled"]; + enableFreeCamera = config["enableFreeCamera"]; + + } + catch (std::exception& e) { + Log::ErrorFmt("LoadConfig error: %s", e.what()); + } + isConfigInit = true; + } +} diff --git a/app/src/main/cpp/GakumasLocalify/config/Config.hpp b/app/src/main/cpp/GakumasLocalify/config/Config.hpp new file mode 100644 index 0000000..1d05ca7 --- /dev/null +++ b/app/src/main/cpp/GakumasLocalify/config/Config.hpp @@ -0,0 +1,15 @@ +#ifndef GAKUMAS_LOCALIFY_CONFIG_HPP +#define GAKUMAS_LOCALIFY_CONFIG_HPP + + +namespace GakumasLocal::Config { + extern bool isConfigInit; + + extern bool enabled; + extern bool enableFreeCamera; + + void LoadConfig(const std::string& configStr); +} + + +#endif //GAKUMAS_LOCALIFY_CONFIG_HPP diff --git a/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp b/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp index 2ef7b8f..7aacda4 100644 --- a/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp +++ b/app/src/main/cpp/deps/UnityResolve/UnityResolve.hpp @@ -45,6 +45,7 @@ #include "xdl.h" #include "../../GakumasLocalify/Log.h" +#include "../../GakumasLocalify/Misc.h" class UnityResolve final { public: diff --git a/app/src/main/cpp/localify.cpp b/app/src/main/cpp/libMarryKotone.cpp similarity index 67% rename from app/src/main/cpp/localify.cpp rename to app/src/main/cpp/libMarryKotone.cpp index f5fae37..ea13a2b 100644 --- a/app/src/main/cpp/localify.cpp +++ b/app/src/main/cpp/libMarryKotone.cpp @@ -5,8 +5,9 @@ #include #include "string" #include "shadowhook.h" - #include "xdl.h" +#include "GakumasLocalify/camera/camera.hpp" +#include "GakumasLocalify/config/Config.hpp" namespace { @@ -24,9 +25,9 @@ namespace xdl_close(m_Il2CppLibrary); } - void InstallHook(void* addr, void* hook, void** orig) override + void* InstallHook(void* addr, void* hook, void** orig) override { - shadowhook_hook_func_addr(addr, hook, orig); + return shadowhook_hook_func_addr(addr, hook, orig); } GakumasLocal::OpaqueFunctionPointer LookupSymbol(const char* name) override @@ -44,8 +45,6 @@ extern "C" JNIEXPORT void JNICALL Java_io_github_chinosk_gakumas_localify_GakumasHookMain_initHook(JNIEnv *env, jclass clazz, jstring targetLibraryPath, jstring localizationFilesDir) { - GakumasLocal::Log::Info("Hello initHook!"); - const auto targetLibraryPathChars = env->GetStringUTFChars(targetLibraryPath, nullptr); const std::string targetLibraryPathStr = targetLibraryPathChars; @@ -54,4 +53,19 @@ Java_io_github_chinosk_gakumas_localify_GakumasHookMain_initHook(JNIEnv *env, jc auto& plugin = GakumasLocal::Plugin::GetInstance(); plugin.InstallHook(std::make_unique(targetLibraryPathStr, localizationFilesDirCharsStr)); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_github_chinosk_gakumas_localify_GakumasHookMain_keyboardEvent(JNIEnv *env, jclass clazz, jint key_code, jint action) { + GKCamera::on_cam_rawinput_keyboard(action, key_code); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_github_chinosk_gakumas_localify_GakumasHookMain_loadConfig(JNIEnv *env, jclass clazz, + jstring config_json_str) { + const auto configJsonStrChars = env->GetStringUTFChars(config_json_str, nullptr); + const std::string configJson = configJsonStrChars; + GakumasLocal::Config::LoadConfig(configJson); } \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt index 11ad4f5..a8b6e9f 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/GakumasHookMain.kt @@ -1,8 +1,13 @@ package io.github.chinosk.gakumas.localify import android.annotation.SuppressLint +import android.app.Activity import android.app.AndroidAppHelper import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.os.Handler +import android.os.Looper import android.util.Log import com.bytedance.shadowhook.ShadowHook import com.bytedance.shadowhook.ShadowHook.ConfigBuilder @@ -12,20 +17,63 @@ import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker -import kotlinx.coroutines.currentCoroutineContext +import android.view.KeyEvent +import android.widget.Toast +import com.google.gson.Gson +import de.robv.android.xposed.XposedBridge +import io.github.chinosk.gakumas.localify.models.GakumasConfig import java.io.File class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { private lateinit var modulePath: String + private var nativeLibLoadSuccess: Boolean private var alreadyInitialized = false private val TAG = "GakumasLocalify" + private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen" + private val nativeLibName = "MarryKotone" + + private var gkmsDataInited = false override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { - if (lpparam.packageName != "com.bandainamcoent.idolmaster_gakuen") { + if (lpparam.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 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 + initGkmsConfig(currActivity) + } + }) + + XposedBridge.hookAllMethods(appActivityClass, "onResume", object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + Log.d(TAG, "onResume") + val currActivity = param.thisObject as Activity + initGkmsConfig(currActivity) + } + }) + val cls = lpparam.classLoader.loadClass("com.unity3d.player.UnityPlayer") XposedHelpers.findAndHookMethod( cls, @@ -43,17 +91,66 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { } val app = AndroidAppHelper.currentApplication() + if (nativeLibLoadSuccess) { + showToast("lib$nativeLibName.so 已加载") + } + else { + showToast("加载 native 库 lib$nativeLibName.so 失败") + return + } + + if (!gkmsDataInited) { + requestConfig(app.applicationContext) + } FilesChecker.initAndCheck(app.filesDir, modulePath) - - initHook("${app.applicationInfo.nativeLibraryDir}/libil2cpp.so", - File(app.filesDir.absolutePath, FilesChecker.localizationFilesDir).absolutePath) + initHook( + "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so", + File( + app.filesDir.absolutePath, + FilesChecker.localizationFilesDir + ).absolutePath + ) alreadyInitialized = true } }) } + private fun showToast(message: String) { + val app = AndroidAppHelper.currentApplication() + val context = app?.applicationContext + if (context != null) { + val handler = Handler(Looper.getMainLooper()) + handler.post { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + else { + Log.e(TAG, "showToast: $message failed: applicationContext is null") + } + } + + fun initGkmsConfig(activity: Activity) { + val intent = activity.intent + val gkmsData = intent.getStringExtra("gkmsData") + if (gkmsData != null) { + gkmsDataInited = true + loadConfig(gkmsData) + Log.d(TAG, "gkmsData: $gkmsData") + } + } + + fun requestConfig(activity: Context) { + val intent = Intent().apply { + setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.MainActivity") + putExtra("gkmsData", "芜湖") + flags = FLAG_ACTIVITY_NEW_TASK + } + // activity.startActivityForResult(intent, 114514) + activity.startActivity(intent) + } + override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { modulePath = startupParam.modulePath } @@ -61,6 +158,10 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { companion object { @JvmStatic external fun initHook(targetLibraryPath: String, localizationFilesDir: String) + @JvmStatic + external fun keyboardEvent(keyCode: Int, action: Int) + @JvmStatic + external fun loadConfig(configJsonStr: String) } @@ -70,6 +171,12 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit { .setMode(ShadowHook.Mode.UNIQUE) .build() ) - System.loadLibrary("MarryKotone") + + nativeLibLoadSuccess = try { + System.loadLibrary(nativeLibName) + true + } catch (e: UnsatisfiedLinkError) { + false + } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt index cbbf2df..6487058 100644 --- a/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/MainActivity.kt @@ -1,48 +1,94 @@ package io.github.chinosk.gakumas.localify -import android.os.Bundle -import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import io.github.chinosk.gakumas.localify.ui.theme.GakumasLocalifyTheme -class MainActivity : ComponentActivity() { +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import io.github.chinosk.gakumas.localify.databinding.ActivityMainBinding +import io.github.chinosk.gakumas.localify.models.GakumasConfig +import java.io.File + + +interface ConfigListener { + fun onClickStartGame() + fun onEnabledChanged(value: Boolean) + fun onEnableFreeCameraChanged(value: Boolean) +} + +class MainActivity : AppCompatActivity(), ConfigListener { + private lateinit var config: GakumasConfig + private val TAG = "GakumasLocalify" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.i("GakumasLocal", "Hello Producer!") - setContent { - GakumasLocalifyTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Producer") - } - } + setContentView(R.layout.activity_main) + + loadConfig() + + val requestData = intent.getStringExtra("gkmsData") + if (requestData != null) { + onClickStartGame() + finish() + } + + val binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.config = config + binding.listener = this + } + + private fun showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + + private fun getConfigContent(): String { + val configFile = File(filesDir, "gkms-config.json") + return if (configFile.exists()) { + configFile.readText() + } + else { + showToast("检测到第一次启动,初始化配置文件...") + "{}" } } -} -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} + private fun loadConfig() { + val configStr = getConfigContent() + val config = try { + Gson().fromJson(configStr, GakumasConfig::class.java) + } + catch (e: JsonSyntaxException) { + showToast("配置文件异常,已重置: $e") + Gson().fromJson("{}", GakumasConfig::class.java) + } + this.config = config + saveConfig() + } -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - GakumasLocalifyTheme { - Greeting("Producer") + private fun saveConfig() { + val configFile = File(filesDir, "gkms-config.json") + configFile.writeText(Gson().toJson(config)) + } + + override fun onEnabledChanged(value: Boolean) { + config.enabled = value + saveConfig() + } + + override fun onEnableFreeCameraChanged(value: Boolean) { + config.enableFreeCamera = value + saveConfig() + } + + override fun onClickStartGame() { + val intent = Intent().apply { + setClassName("com.bandainamcoent.idolmaster_gakuen", "com.google.firebase.MessagingUnityPlayerActivity") + putExtra("gkmsData", getConfigContent()) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) } } \ No newline at end of file diff --git a/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt new file mode 100644 index 0000000..425e6c5 --- /dev/null +++ b/app/src/main/java/io/github/chinosk/gakumas/localify/models/GakumasConfig.kt @@ -0,0 +1,7 @@ +package io.github.chinosk.gakumas.localify.models + +data class GakumasConfig( + var enabled: Boolean = true, + var enableFreeCamera: Boolean = false +) + diff --git a/app/src/main/play_store_512.png b/app/src/main/play_store_512.png new file mode 100644 index 0000000..8e56686 Binary files /dev/null and b/app/src/main/play_store_512.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7c8a8ee --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..90f9580 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c0d463e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000..a7b3850 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000..bff158f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..626e582 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000..904f271 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000..6798d82 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..5e6ec7e Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000..95a6b1f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000..3acb563 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..adb6e98 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000..2b4543f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000..4d2c832 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..6eead49 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png new file mode 100644 index 0000000..b28ec21 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png new file mode 100644 index 0000000..8fc28b0 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9b4565..1d39acb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,7 @@ Gakumas Localify + Gakumas Localify + 启用插件 (不可热重载) + 启用自由视角(可热重载; 需使用实体键盘) + 以上述配置启动游戏/重载配置 \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e70468a..9e0090c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ +