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 @@
+
-
\ No newline at end of file