feat: PC 版 Live/拍摄陀螺仪模拟(右键拖动 + 小键盘)

Windows 没有陀螺仪,Live 全景拍摄的陀螺仪按钮在 DMM 版被禁用。
新增 enableGyroEmu 开关(GUI 摄像机设置,含灵敏度与轴反转),打开后
按住鼠标右键拖动转动视角,小键盘 4/6 8/2 左右上下、7/9 翻滚、5 复位。

实现分三层,每层都有过失败尝试,原因记录在注释里:
- GyroIcall:改写 il2cpp icall 包装里的 static 缓存指针,向
  SystemInfo.IsGyroAvailable / Gyroscope.getEnabled/setEnabled/
  attitude/gravity 提供假读数。不能 hook 原生 icall——Windows 上它们是
  return 常量的桩,被链接器 ICF 合并进 IsDebugBuild 等无关函数,改一个
  等于改全局(实测 [Debug] 变 True、开场黑屏);也不能 hook 托管包装——
  PC 版 MSVC 把它们内联进了调用方,实测一次不被调用。
- GyroForceInit:PC 版把 IsPanoramaGyroDisabledOnPlatform() 内联成
  常量 true,CampusGyroController 根本不会创建,IsGyroReady 永远 false。
  在 LivePanoramaCameraManager.LateUpdate 里自己 new 出控制器和 VLGyro,
  按 3.3.0 布局置字段后复用游戏自己的 ResetRotation。
- 两个方法体足够大、实测会被调用的托管 hook 放开 UI 闸门。

姿态换算来自反汇编:gyroRotationRaw = base ⊗ (-a.x, -a.y, a.z, a.w),
相对旋转里 base 被消掉,因此 attitude 直接编码相机欧拉角。

顺带:Windows 版日志同步写 gakumas-local/log.txt,便于无控制台排查。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
chihya72andClaude Fable 5.1 committed 2026-09-08 17:17:40 +08:00
1 parent 937c45ba8c
commit 860f02d32a
12 files changed
+284

No files matched your search

+4
View File
@@ -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,
+66
View File
@@ -0,0 +1,66 @@
#include "GyroEmu.hpp"
#include "config/Config.hpp"
#include <windows.h>
#include <algorithm>
#include <chrono>
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<float>(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);
}
}
+10
View File
@@ -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();
}
+155
View File
@@ -12,6 +12,7 @@
#include <atomic>
#include <chrono>
#include <cmath>
#include <climits>
#include <ctime>
#include <cstring>
#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 <typename T> T& F(void* obj, size_t off) { return *reinterpret_cast<T*>(reinterpret_cast<char*>(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<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common", "CampusGyroController", ".ctor"));
static auto gyroCtor = reinterpret_cast<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", ".ctor"));
static auto gyroReset = reinterpret_cast<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", "ResetRotation"));
static auto gyroRaw = reinterpret_cast<Quaternion* (*)(Quaternion*, void*, void*)>(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<void*>(manager, 0x60); // _gyroController
if (!ctrl) { ctrl = ctrlClass->New<void*>(); ctrlCtor(ctrl, nullptr); }
if (F<bool>(ctrl, 0x18)) return; // _initialized
auto& gyro = F<void*>(ctrl, 0x10); // _gyro
if (!gyro) { gyro = gyroClass->New<void*>(); gyroCtor(gyro, nullptr); }
F<int>(gyro, 0x14) = 1; // _updateType = Gyro
F<bool>(gyro, 0x29) = true; // gyroSupported
F<bool>(gyro, 0x2A) = true; // _defaultGyroEnable
for (size_t off : {0x2C, 0x3C, 0x4C, 0x5C}) F<Quaternion>(gyro, off) = identity;
F<bool>(gyro, 0x6C) = true; // _initialized
F<bool>(gyro, 0x6D) = true; // _firstUpdate
gyroReset(gyro, nullptr);
Quaternion raw; gyroRaw(&raw, gyro, nullptr);
F<Quaternion>(ctrl, 0x1C) = Quaternion(-raw.x, -raw.y, -raw.z, raw.w); // _baseRotation = Inverse(raw)
F<int>(ctrl, 0x2C) = INT_MIN; // _lastAccessFrame
F<Quaternion>(ctrl, 0x30) = identity; // _rotation
F<bool>(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 <typename F>
void Patch(const char* label, void* wrapper, F** origOut, F* replacement, const char* icallName) {
*origOut = reinterpret_cast<F*>(Il2cppUtils::il2cpp_resolve_icall(icallName));
if (!wrapper) { Log::ErrorFmt("[GyroEmu] %s: wrapper not found", label); return; }
auto p = reinterpret_cast<uint8_t*>(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<void**>(p + i + 7 + *reinterpret_cast<int32_t*>(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<void*>(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",
+12
View File
@@ -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);
+4
View File
@@ -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;
+10
View File
@@ -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);
}
+5
View File
@@ -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" },
+5
View File
@@ -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", "エラー" },
+5
View File
@@ -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" },
+5
View File
@@ -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" },
+3
View File
@@ -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); }
}