mirror of
https://git.chinosk6.cn/chinosk/gkms-localify-dmm.git
synced 2026-10-06 05:18:06 +07:00
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:
1 parent
937c45ba8c
commit
860f02d32a
12 files changed
+284
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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", "エラー" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
Reference in new issue
Block a user