diff --git a/resource/localizationConfig.json b/resource/localizationConfig.json index a8de5cd..ea05379 100644 --- a/resource/localizationConfig.json +++ b/resource/localizationConfig.json @@ -11,6 +11,10 @@ "gameOrientation": 0, "forceExportResource": false, "enableFreeCamera": false, + "enableGyroEmu": false, + "gyroSensitivity": 1.0, + "gyroInvertX": false, + "gyroInvertY": false, "targetFrameRate": 0, "unlockAllLive": true, "unlockAllLiveCostume": true, diff --git a/src/GakumasLocalify/GyroEmu.cpp b/src/GakumasLocalify/GyroEmu.cpp new file mode 100644 index 0000000..5c38af1 --- /dev/null +++ b/src/GakumasLocalify/GyroEmu.cpp @@ -0,0 +1,66 @@ +#include "GyroEmu.hpp" +#include "config/Config.hpp" +#include +#include +#include + +namespace GakumasLocal::GyroEmu { + using Quaternion = UnityResolve::UnityType::Quaternion; + using Vector3 = UnityResolve::UnityType::Vector3; + + static float pitch = 0, yaw = 0, roll = 0; // 度,Unity 相机欧拉角 + static POINT lastCursor{}; + static bool dragging = false; + static auto lastTick = std::chrono::steady_clock::now(); + + static bool Down(int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; } + + void Reset() { pitch = yaw = roll = 0; } + + static void PollInput() { + const auto now = std::chrono::steady_clock::now(); + const float dt = (std::min)(std::chrono::duration(now - lastTick).count(), 0.1f); + lastTick = now; + + const float sx = Config::gyroSensitivity * (Config::gyroInvertX ? -1.f : 1.f); + const float sy = Config::gyroSensitivity * (Config::gyroInvertY ? -1.f : 1.f); + + // 右键拖动:鼠标位移 -> yaw/pitch + if (Down(VK_RBUTTON)) { + POINT cur; GetCursorPos(&cur); + if (dragging) { + yaw += (cur.x - lastCursor.x) * 0.1f * sx; + pitch += (cur.y - lastCursor.y) * 0.1f * sy; + } + lastCursor = cur; + dragging = true; + } else { + dragging = false; + } + + // 小键盘:4/6 yaw, 8/2 pitch, 7/9 roll, 5 复位 + const float step = 60.f * dt * Config::gyroSensitivity; + if (Down(VK_NUMPAD4)) yaw -= step * sx; + if (Down(VK_NUMPAD6)) yaw += step * sx; + if (Down(VK_NUMPAD8)) pitch -= step * sy; + if (Down(VK_NUMPAD2)) pitch += step * sy; + if (Down(VK_NUMPAD7)) roll += step; + if (Down(VK_NUMPAD9)) roll -= step; + if (Down(VK_NUMPAD5)) Reset(); + + pitch = std::clamp(pitch, -89.f, 89.f); + } + + Quaternion Attitude() { + PollInput(); + Quaternion q; q.Euler(pitch, yaw, roll); + // 反汇编 VLGyro.get_gyroRotationRaw:raw = gyroBaseRotation * Quaternion(-a.x, -a.y, a.z, a.w); + // CampusGyroController.Rotation = Inverse(raw@reset) * raw,base 被消掉,所以直接把相机欧拉角编码进 a 即可。 + return Quaternion(-q.x, -q.y, q.z, q.w); + } + + Vector3 Gravity() { + // ponytail: 竖持手机时重力恒指向设备 -Y,VLGyro 没用到重力,够用 + return Vector3(0.f, -1.f, 0.f); + } +} diff --git a/src/GakumasLocalify/GyroEmu.hpp b/src/GakumasLocalify/GyroEmu.hpp new file mode 100644 index 0000000..b9e41c3 --- /dev/null +++ b/src/GakumasLocalify/GyroEmu.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "../deps/UnityResolve/UnityResolve.hpp" + +// 用鼠标右键拖动 / 小键盘模拟手机陀螺仪姿态(PC 版无陀螺仪) +namespace GakumasLocal::GyroEmu { + // 读一次输入并返回当前 Input.gyro.attitude + UnityResolve::UnityType::Quaternion Attitude(); + UnityResolve::UnityType::Vector3 Gravity(); + void Reset(); +} diff --git a/src/GakumasLocalify/Hook.cpp b/src/GakumasLocalify/Hook.cpp index 1fc283e..8cdc60f 100644 --- a/src/GakumasLocalify/Hook.cpp +++ b/src/GakumasLocalify/Hook.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include "camera/camera.hpp" @@ -29,6 +30,7 @@ #ifdef GKMS_WINDOWS #include "../windowsPlatform.hpp" + #include "GyroEmu.hpp" #include "cpprest/details/http_helpers.h" #include "../resourceUpdate/resourceUpdate.hpp" #endif @@ -212,6 +214,151 @@ namespace GakumasLocal::HookMain { // 从游戏自己的调用里捕获真实对象指针: // Camera.get_main / Component.get_transform 是带 MethodInfo* 尾参的 managed 方法, // 用 hook(而不是 UnityResolve 的 managed Invoke)才能拿到可靠结果。 +#ifdef GKMS_WINDOWS + // 陀螺仪模拟(PC 无陀螺仪),三层: + // 1. GyroIcall —— 替换 il2cpp icall 缓存指针提供假读数。不能 hook 原生 icall(Windows 上它们是 return 常量 + // 的桩,被链接器 ICF 合并进 IsDebugBuild 等,实测 [Debug] 变 True、开场黑屏),也不能 hook + // 托管包装(PC 上被内联,实测一次不被调用)。 + // 2. GyroForceInit —— PC 把 IsPanoramaGyroDisabledOnPlatform() 内联成常量 true,控制器根本不创建,LateUpdate 里自己建。 + // 3. 两个方法体足够大、实测会被调用的托管 hook 放开 UI 闸门。 +#define GYRO_LOG_ONCE(name) { static bool logged = false; if (!logged) { logged = true; Log::InfoFmt("[GyroEmu] %s called (emu=%d)", name, (int)Config::enableGyroEmu); } } + + DEFINE_HOOK(void, LiveScenePanoramaContentPresenter_Initialize, (void* self, void* screenRoot, bool isGyroSupported, void* mtd)) { + GYRO_LOG_ONCE("PanoramaContentPresenter.Initialize"); + LiveScenePanoramaContentPresenter_Initialize_Orig(self, screenRoot, isGyroSupported || Config::enableGyroEmu, mtd); + } + + DEFINE_HOOK(bool, Photography3DPresenter_get_IsGyroSupported, (void* self, void* mtd)) { + GYRO_LOG_ONCE("Photography3DPresenter.IsGyroSupported"); + if (Config::enableGyroEmu) return true; + return Photography3DPresenter_get_IsGyroSupported_Orig(self, mtd); + } + + // PC 版把 LiveUtility.IsPanoramaGyroDisabledOnPlatform()(恒 true)内联成常量,CampusGyroController 根本不会被 + // 创建/初始化(log 证实 IsGyroAvailable 从未被查询),IsGyroReady 永远 false。这里在 LateUpdate 里自己把它建出来。 + // 字段偏移来自 3.3.0 il2cpp.h(CampusGyroController / VLGyro / LivePanoramaCameraManager)。 + namespace GyroForceInit { + using Quaternion = UnityResolve::UnityType::Quaternion; + template T& F(void* obj, size_t off) { return *reinterpret_cast(reinterpret_cast(obj) + off); } + + void Ensure(void* manager) { + static auto ctrlClass = Il2cppUtils::GetClass("campus-submodule.Runtime.dll", "Campus.Common", "CampusGyroController"); + static auto gyroClass = Il2cppUtils::GetClass("vl-unity.Runtime.dll", "VL", "VLGyro"); + static auto ctrlCtor = reinterpret_cast(Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common", "CampusGyroController", ".ctor")); + static auto gyroCtor = reinterpret_cast(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", ".ctor")); + static auto gyroReset = reinterpret_cast(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", "ResetRotation")); + static auto gyroRaw = reinterpret_cast(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", "get_gyroRotationRaw")); + static bool reported = false; + if (!ctrlClass || !gyroClass || !ctrlCtor || !gyroCtor || !gyroReset || !gyroRaw) { + if (!reported) { reported = true; Log::ErrorFmt("[GyroEmu] force-init: missing class/method (%p %p %p %p %p %p)", ctrlClass, gyroClass, ctrlCtor, gyroCtor, gyroReset, gyroRaw); } + return; + } + const Quaternion identity(0, 0, 0, 1); + auto& ctrl = F(manager, 0x60); // _gyroController + if (!ctrl) { ctrl = ctrlClass->New(); ctrlCtor(ctrl, nullptr); } + if (F(ctrl, 0x18)) return; // _initialized + auto& gyro = F(ctrl, 0x10); // _gyro + if (!gyro) { gyro = gyroClass->New(); gyroCtor(gyro, nullptr); } + F(gyro, 0x14) = 1; // _updateType = Gyro + F(gyro, 0x29) = true; // gyroSupported + F(gyro, 0x2A) = true; // _defaultGyroEnable + for (size_t off : {0x2C, 0x3C, 0x4C, 0x5C}) F(gyro, off) = identity; + F(gyro, 0x6C) = true; // _initialized + F(gyro, 0x6D) = true; // _firstUpdate + gyroReset(gyro, nullptr); + Quaternion raw; gyroRaw(&raw, gyro, nullptr); + F(ctrl, 0x1C) = Quaternion(-raw.x, -raw.y, -raw.z, raw.w); // _baseRotation = Inverse(raw) + F(ctrl, 0x2C) = INT_MIN; // _lastAccessFrame + F(ctrl, 0x30) = identity; // _rotation + F(ctrl, 0x18) = true; // _initialized + Log::InfoFmt("[GyroEmu] force-init done: controller %p gyro %p", ctrl, gyro); + } + } + + DEFINE_HOOK(void, LivePanoramaCameraManager_LateUpdate, (void* self, void* mtd)) { + if (Config::enableGyroEmu && self) GyroForceInit::Ensure(self); + LivePanoramaCameraManager_LateUpdate_Orig(self, mtd); + } + + // PC 版 MSVC 把 SystemInfo.supportsGyroscope / Gyroscope.* 这些托管包装内联进了调用方,hook 函数本体拦不到。 + // il2cpp 的 icall 包装里有一个 static 缓存指针(_il2cpp_icall_func),内联后所有副本共用同一个变量, + // 直接把这个变量改成我们的函数即可,不碰任何机器码,也不受链接器 ICF 合并影响。 + namespace GyroIcall { + using Quaternion = UnityResolve::UnityType::Quaternion; + using Vector3 = UnityResolve::UnityType::Vector3; + bool (*orig_IsGyroAvailable)() = nullptr; + bool (*orig_getEnabled)(int) = nullptr; + void (*orig_setEnabled)(int, bool) = nullptr; + void (*orig_attitude)(int, Quaternion*) = nullptr; + void (*orig_gravity)(int, Vector3*) = nullptr; + + bool IsGyroAvailable() { + GYRO_LOG_ONCE("icall SystemInfo.IsGyroAvailable"); + if (Config::enableGyroEmu) return true; + return orig_IsGyroAvailable ? orig_IsGyroAvailable() : false; + } + bool getEnabled(int idx) { + GYRO_LOG_ONCE("icall Gyroscope.getEnabled"); + if (Config::enableGyroEmu) return true; + return orig_getEnabled ? orig_getEnabled(idx) : false; + } + void setEnabled(int idx, bool v) { + GYRO_LOG_ONCE("icall Gyroscope.setEnabled"); + if (Config::enableGyroEmu) return; // Windows 无传感器对象,原生实现解空指针 + if (orig_setEnabled) orig_setEnabled(idx, v); + } + void attitude(int idx, Quaternion* ret) { + GYRO_LOG_ONCE("icall Gyroscope.attitude"); + if (Config::enableGyroEmu) { *ret = GyroEmu::Attitude(); return; } + if (orig_attitude) orig_attitude(idx, ret); else *ret = Quaternion(0, 0, 0, 1); + } + void gravity(int idx, Vector3* ret) { + GYRO_LOG_ONCE("icall Gyroscope.gravity"); + if (Config::enableGyroEmu) { *ret = GyroEmu::Gravity(); return; } + if (orig_gravity) orig_gravity(idx, ret); else *ret = Vector3(0, 0, 0); + } + + // 在 icall 包装函数体里找 `mov rXX,[rip+disp]; test rXX,rXX` 定位缓存变量,写入替换函数 + template + void Patch(const char* label, void* wrapper, F** origOut, F* replacement, const char* icallName) { + *origOut = reinterpret_cast(Il2cppUtils::il2cpp_resolve_icall(icallName)); + if (!wrapper) { Log::ErrorFmt("[GyroEmu] %s: wrapper not found", label); return; } + auto p = reinterpret_cast(wrapper); + for (int i = 0; i < 0x80; i++) { + if (p[i] != 0x48 || p[i + 1] != 0x8B || (p[i + 2] & 0xC7) != 0x05) continue; + const int reg = (p[i + 2] >> 3) & 7; + const uint8_t testModrm = 0xC0 | (reg << 3) | reg; + bool ok = false; + for (int j = i + 7; j <= i + 7 + 8; j++) + if (p[j] == 0x48 && p[j + 1] == 0x85 && p[j + 2] == testModrm) { ok = true; break; } + if (!ok) continue; + auto cache = reinterpret_cast(p + i + 7 + *reinterpret_cast(p + i + 3)); + MEMORY_BASIC_INFORMATION mbi{}; + if (!VirtualQuery(cache, &mbi, sizeof(mbi)) || !(mbi.Protect & (PAGE_READWRITE | PAGE_WRITECOPY))) { + Log::ErrorFmt("[GyroEmu] %s: cache %p not writable", label, cache); return; + } + Log::InfoFmt("[GyroEmu] %s: icall cache %p (was %p, orig %p) -> %p", label, cache, *cache, *origOut, replacement); + *cache = reinterpret_cast(replacement); + return; + } + Log::ErrorFmt("[GyroEmu] %s: icall cache pattern not found at %p", label, wrapper); + } + + void Install() { + Patch("IsGyroAvailable", Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "SystemInfo", "IsGyroAvailable"), + &orig_IsGyroAvailable, &IsGyroAvailable, "UnityEngine.SystemInfo::IsGyroAvailable()"); + Patch("getEnabled_Internal", Il2cppUtils::GetMethodPointer("UnityEngine.InputLegacyModule.dll", "UnityEngine", "Gyroscope", "getEnabled_Internal"), + &orig_getEnabled, &getEnabled, "UnityEngine.Gyroscope::getEnabled_Internal(System.Int32)"); + Patch("setEnabled_Internal", Il2cppUtils::GetMethodPointer("UnityEngine.InputLegacyModule.dll", "UnityEngine", "Gyroscope", "setEnabled_Internal"), + &orig_setEnabled, &setEnabled, "UnityEngine.Gyroscope::setEnabled_Internal(System.Int32,System.Boolean)"); + Patch("attitude_Internal_Injected", Il2cppUtils::GetMethodPointer("UnityEngine.InputLegacyModule.dll", "UnityEngine", "Gyroscope", "attitude_Internal_Injected"), + &orig_attitude, &attitude, "UnityEngine.Gyroscope::attitude_Internal_Injected(System.Int32,UnityEngine.Quaternion&)"); + Patch("gravity_Internal_Injected", Il2cppUtils::GetMethodPointer("UnityEngine.InputLegacyModule.dll", "UnityEngine", "Gyroscope", "gravity_Internal_Injected"), + &orig_gravity, &gravity, "UnityEngine.Gyroscope::gravity_Internal_Injected(System.Int32,UnityEngine.Vector3&)"); + } + } +#endif + DEFINE_HOOK(UnityResolve::UnityType::Camera*, Camera_get_main, (void* mtd)) { auto ret = Camera_get_main_Orig(mtd); if (Config::enableFreeCamera && ret && ret != mainCameraCache) { @@ -2890,6 +3037,14 @@ namespace GakumasLocal::HookMain { "RenderPipeline", "EndCameraRendering")); #ifdef GKMS_WINDOWS + ADD_HOOK(LiveScenePanoramaContentPresenter_Initialize, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Live", + "LiveScenePanoramaContentPresenter", "Initialize")); + ADD_HOOK(Photography3DPresenter_get_IsGyroSupported, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Photography", + "Photography3DPresenter", "get_IsGyroSupported")); + GyroIcall::Install(); + ADD_HOOK(LivePanoramaCameraManager_LateUpdate, Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Live", + "LivePanoramaCameraManager", "LateUpdate")); + ADD_HOOK(WindowHandle_SetWindowLong, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Common.StandAloneWindow", "WindowHandle", "SetWindowLong")); //ADD_HOOK(WindowHandle_SetWindowLong32, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Common.StandAloneWindow", diff --git a/src/GakumasLocalify/config/Config.cpp b/src/GakumasLocalify/config/Config.cpp index f1bacea..3c11592 100644 --- a/src/GakumasLocalify/config/Config.cpp +++ b/src/GakumasLocalify/config/Config.cpp @@ -19,6 +19,10 @@ namespace GakumasLocal::Config { bool dumpText = false; bool dumpRuntimeTexture = false; bool enableFreeCamera = false; + bool enableGyroEmu = false; + float gyroSensitivity = 1.0f; + bool gyroInvertX = false; + bool gyroInvertY = false; int targetFrameRate = 0; bool unlockAllLive = false; bool unlockAllLiveCostume = false; @@ -77,6 +81,10 @@ namespace GakumasLocal::Config { GetConfigItem(dumpRuntimeTexture); GetConfigItem(targetFrameRate); GetConfigItem(enableFreeCamera); + GetConfigItem(enableGyroEmu); + GetConfigItem(gyroSensitivity); + GetConfigItem(gyroInvertX); + GetConfigItem(gyroInvertY); GetConfigItem(unlockAllLive); GetConfigItem(unlockAllLiveCostume); GetConfigItem(enableLiveCustomeDress); @@ -135,6 +143,10 @@ namespace GakumasLocal::Config { SetConfigItem(dumpRuntimeTexture); SetConfigItem(targetFrameRate); SetConfigItem(enableFreeCamera); + SetConfigItem(enableGyroEmu); + SetConfigItem(gyroSensitivity); + SetConfigItem(gyroInvertX); + SetConfigItem(gyroInvertY); SetConfigItem(unlockAllLive); SetConfigItem(unlockAllLiveCostume); SetConfigItem(enableLiveCustomeDress); diff --git a/src/GakumasLocalify/config/Config.hpp b/src/GakumasLocalify/config/Config.hpp index 348f0e3..b5a38cb 100644 --- a/src/GakumasLocalify/config/Config.hpp +++ b/src/GakumasLocalify/config/Config.hpp @@ -15,6 +15,10 @@ namespace GakumasLocal::Config { extern bool dumpText; extern bool dumpRuntimeTexture; extern bool enableFreeCamera; + extern bool enableGyroEmu; + extern float gyroSensitivity; + extern bool gyroInvertX; + extern bool gyroInvertY; extern int targetFrameRate; extern bool unlockAllLive; extern bool unlockAllLiveCostume; diff --git a/src/gkmsGUI/gkmsGUILoop.cpp b/src/gkmsGUI/gkmsGUILoop.cpp index 624a0b0..ebe8561 100644 --- a/src/gkmsGUI/gkmsGUILoop.cpp +++ b/src/gkmsGUI/gkmsGUILoop.cpp @@ -318,6 +318,16 @@ namespace GkmsGUILoop { if (ImGui::CollapsingHeader(ts("camera_settings"), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(indentWidth); ImGui::Checkbox(ts("enable_free_camera"), &Config::enableFreeCamera); + ImGui::Checkbox(ts("enable_gyro_emu"), &Config::enableGyroEmu); + if (Config::enableGyroEmu) { + ImGui::Indent(indentWidth); + ImGui::TextWrapped("%s", ts("gyro_emu_help")); + ImGui::SliderFloat(ts("gyro_sensitivity"), &Config::gyroSensitivity, 0.1f, 5.0f); + ImGui::Checkbox(ts("gyro_invert_x"), &Config::gyroInvertX); + ImGui::SameLine(); + ImGui::Checkbox(ts("gyro_invert_y"), &Config::gyroInvertY); + ImGui::Unindent(indentWidth); + } ImGui::Unindent(indentWidth); } diff --git a/src/gkmsGUI/i18nData/strings_en.hpp b/src/gkmsGUI/i18nData/strings_en.hpp index 94fa3a1..770a5ab 100644 --- a/src/gkmsGUI/i18nData/strings_en.hpp +++ b/src/gkmsGUI/i18nData/strings_en.hpp @@ -16,6 +16,11 @@ namespace I18nData { { "replace_texture", "Replace Texture" }, { "lazy_init", "Fast Initialization (Lazy loading)" }, { "enable_free_camera", "Enable Free Camera" }, + { "enable_gyro_emu", "Enable Gyroscope Emulation (Live/Photo)" }, + { "gyro_emu_help", "Hold right mouse button and drag to rotate; Numpad 4/6 8/2 yaw/pitch, 7/9 roll, 5 reset" }, + { "gyro_sensitivity", "Gyro Sensitivity" }, + { "gyro_invert_x", "Invert X" }, + { "gyro_invert_y", "Invert Y" }, { "start_game", "Start Game / Hot Reload Config" }, { "setFpsTitle", "Max FPS (0 is Use Original Settings)" }, { "unlockAllLive", "Unlock All Live" }, diff --git a/src/gkmsGUI/i18nData/strings_ja.hpp b/src/gkmsGUI/i18nData/strings_ja.hpp index 48cd54d..e06b5c6 100644 --- a/src/gkmsGUI/i18nData/strings_ja.hpp +++ b/src/gkmsGUI/i18nData/strings_ja.hpp @@ -57,6 +57,11 @@ namespace I18nData { { "dropdown_menu", "ドロップダウンメニュー" }, { "enable_breast_param", "胸のパラメーターを有効化" }, { "enable_free_camera", "フリーカメラを有効化" }, + { "enable_gyro_emu", "ジャイロ模擬を有効化(ライブ/撮影)" }, + { "gyro_emu_help", "右クリックドラッグで視点回転;テンキー 4/6 8/2 左右上下, 7/9 ロール, 5 リセット" }, + { "gyro_sensitivity", "ジャイロ感度" }, + { "gyro_invert_x", "左右反転" }, + { "gyro_invert_y", "上下反転" }, { "enable_plugin", "プラグイン有効化 (ホットリロードなし)" }, { "error_a11y_label", "エラー: 無効" }, { "error_icon_content_description", "エラー" }, diff --git a/src/gkmsGUI/i18nData/strings_zh-rCN.hpp b/src/gkmsGUI/i18nData/strings_zh-rCN.hpp index f9c890c..6b54102 100644 --- a/src/gkmsGUI/i18nData/strings_zh-rCN.hpp +++ b/src/gkmsGUI/i18nData/strings_zh-rCN.hpp @@ -16,6 +16,11 @@ namespace I18nData { { "replace_texture", "替换贴图" }, { "lazy_init", "快速初始化(懒加载配置)" }, { "enable_free_camera", "启用自由视角(可热重载; 需使用实体键盘)" }, + { "enable_gyro_emu", "启用陀螺仪模拟(Live/拍摄)" }, + { "gyro_emu_help", "按住鼠标右键拖动转动视角;小键盘 4/6 8/2 左右上下, 7/9 翻滚, 5 复位" }, + { "gyro_sensitivity", "陀螺仪灵敏度" }, + { "gyro_invert_x", "反转左右" }, + { "gyro_invert_y", "反转上下" }, { "start_game", "以上述配置启动游戏/重载配置" }, { "setFpsTitle", "最大 FPS (0 为保持游戏原设置)" }, { "unlockAllLive", "解锁所有 Live" }, diff --git a/src/gkmsGUI/i18nData/strings_zh-rTW.hpp b/src/gkmsGUI/i18nData/strings_zh-rTW.hpp index 13fe5cf..3606db0 100644 --- a/src/gkmsGUI/i18nData/strings_zh-rTW.hpp +++ b/src/gkmsGUI/i18nData/strings_zh-rTW.hpp @@ -16,6 +16,11 @@ namespace I18nData { { "replace_texture", "替換貼圖" }, { "lazy_init", "快速初始化(懶人設定)" }, { "enable_free_camera", "啟用自由視角(可熱重載; 需使用實體鍵盤)" }, + { "enable_gyro_emu", "啟用陀螺儀模擬(Live/拍攝)" }, + { "gyro_emu_help", "按住滑鼠右鍵拖動轉動視角;數字鍵盤 4/6 8/2 左右上下, 7/9 翻滾, 5 重設" }, + { "gyro_sensitivity", "陀螺儀靈敏度" }, + { "gyro_invert_x", "反轉左右" }, + { "gyro_invert_y", "反轉上下" }, { "start_game", "以上述設定啟動遊戲/重載設定" }, { "setFpsTitle", "最大 FPS (0 為保持遊戲原設定)" }, { "unlockAllLive", "解鎖所有 Live" }, diff --git a/src/platformDefine.hpp b/src/platformDefine.hpp index 219fb10..3d6050d 100644 --- a/src/platformDefine.hpp +++ b/src/platformDefine.hpp @@ -41,4 +41,7 @@ static void __android_log_write(int prio, const char* tag, const char* msg) { prio = 1; } printf("[%s] %s: %s\n", logLevels[prio], tag, msg); + // ponytail: 同步落盘,方便无控制台时排查 hook 是否装上 + static FILE* logFile = fopen("gakumas-local/log.txt", "w"); + if (logFile) { fprintf(logFile, "[%s] %s: %s\n", logLevels[prio], tag, msg); fflush(logFile); } }