mirror of
https://git.chinosk6.cn/chinosk/gkms-local.git
synced 2026-10-06 05:18:08 +07:00
fix: restore Android gyro and meishi fixes for v3.5.0
This commit is contained in:
1 parent
ebfede9093
commit
c07adf7a20
6 files changed
+473
-11
No files matched your search
+2
-2
@@ -15,8 +15,8 @@ android {
|
||||
applicationId "io.github.chinosk.gakumas.localify"
|
||||
minSdk 29
|
||||
targetSdk 34
|
||||
versionCode 12
|
||||
versionName "v3.4.1"
|
||||
versionCode 13
|
||||
versionName "v3.5.0"
|
||||
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
// 小键盘:7/9 翻滚, 5 复位。4/6/8/2 是游戏自己拍摄页的镜头键,不占用
|
||||
const float step = 60.f * dt * Config::gyroSensitivity;
|
||||
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,191 @@ 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 的 InitializeAsync
|
||||
// 被整段跳过(log 证实 IsGyroAvailable 从未被查询),IsGyroReady 永远 false。Live 与拍摄各持有一个控制器,
|
||||
// 所以在构造函数 hook 里统一把它置成初始化完成态,再在两条链路各留一个“控制器为空就自己建”的兜底。
|
||||
// 字段偏移来自 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); }
|
||||
|
||||
struct Api {
|
||||
UnityResolve::Class* ctrlClass; UnityResolve::Class* gyroClass;
|
||||
void (*ctrlCtor)(void*, void*); void (*gyroCtor)(void*, void*); void (*gyroReset)(void*, void*);
|
||||
Quaternion* (*gyroRaw)(Quaternion*, void*, void*);
|
||||
};
|
||||
static const Api* GetApi() {
|
||||
static Api api{
|
||||
Il2cppUtils::GetClass("campus-submodule.Runtime.dll", "Campus.Common", "CampusGyroController"),
|
||||
Il2cppUtils::GetClass("vl-unity.Runtime.dll", "VL", "VLGyro"),
|
||||
reinterpret_cast<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common", "CampusGyroController", ".ctor")),
|
||||
reinterpret_cast<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", ".ctor")),
|
||||
reinterpret_cast<void (*)(void*, void*)>(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", "ResetRotation")),
|
||||
reinterpret_cast<Quaternion* (*)(Quaternion*, void*, void*)>(Il2cppUtils::GetMethodPointer("vl-unity.Runtime.dll", "VL", "VLGyro", "get_gyroRotationRaw")),
|
||||
};
|
||||
static const bool ok = [] {
|
||||
if (api.ctrlClass && api.gyroClass && api.ctrlCtor && api.gyroCtor && api.gyroReset && api.gyroRaw) return true;
|
||||
Log::ErrorFmt("[GyroEmu] force-init: missing class/method (%p %p %p %p %p %p)", api.ctrlClass, api.gyroClass, api.ctrlCtor, api.gyroCtor, api.gyroReset, api.gyroRaw);
|
||||
return false;
|
||||
}();
|
||||
return ok ? &api : nullptr;
|
||||
}
|
||||
|
||||
// 把一个已构造的 CampusGyroController 直接置成“初始化完成”态(等价于它自己 InitializeAsync 成功后的状态)
|
||||
void InitController(void* ctrl) {
|
||||
auto api = GetApi();
|
||||
if (!api || !ctrl || F<bool>(ctrl, 0x18)) return; // _initialized
|
||||
const Quaternion identity(0, 0, 0, 1);
|
||||
auto& gyro = F<void*>(ctrl, 0x10); // _gyro
|
||||
if (!gyro) { gyro = api->gyroClass->New<void*>(); api->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
|
||||
api->gyroReset(gyro, nullptr);
|
||||
Quaternion raw; api->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);
|
||||
}
|
||||
|
||||
void* NewController() {
|
||||
auto api = GetApi();
|
||||
if (!api) return nullptr;
|
||||
auto ctrl = api->ctrlClass->New<void*>();
|
||||
api->ctrlCtor(ctrl, nullptr); // 构造函数 hook 会顺手 InitController
|
||||
InitController(ctrl);
|
||||
return ctrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有 CampusGyroController 一出生就初始化完成,Live / 拍摄 / 其他持有者都不用再关心 PC 上被跳过的 InitializeAsync
|
||||
DEFINE_HOOK(void, CampusGyroController_ctor, (void* self, void* mtd)) {
|
||||
CampusGyroController_ctor_Orig(self, mtd);
|
||||
if (Config::enableGyroEmu) GyroForceInit::InitController(self);
|
||||
}
|
||||
|
||||
// Live:PC 版可能连控制器都没建,LateUpdate 里兜底
|
||||
DEFINE_HOOK(void, LivePanoramaCameraManager_LateUpdate, (void* self, void* mtd)) {
|
||||
if (Config::enableGyroEmu && self) {
|
||||
auto& ctrl = GyroForceInit::F<void*>(self, 0x60); // _gyroController
|
||||
if (!ctrl) ctrl = GyroForceInit::NewController();
|
||||
else GyroForceInit::InitController(ctrl);
|
||||
}
|
||||
LivePanoramaCameraManager_LateUpdate_Orig(self, mtd);
|
||||
}
|
||||
|
||||
// 拍摄:PhotographySceneManager 把控制器交给 PhotographyActivityController,这里是它进相机控制器的唯一入口
|
||||
DEFINE_HOOK(void, PhotographyActivityController_Initialize, (void* self, void* volumeProfile, void* gyroController, float arg, void* mtd)) {
|
||||
if (Config::enableGyroEmu) {
|
||||
if (!gyroController) gyroController = GyroForceInit::NewController();
|
||||
else GyroForceInit::InitController(gyroController);
|
||||
}
|
||||
PhotographyActivityController_Initialize_Orig(self, volumeProfile, gyroController, arg, 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) {
|
||||
@@ -869,7 +1056,16 @@ namespace GakumasLocal::HookMain {
|
||||
}
|
||||
#endif
|
||||
|
||||
std::unordered_set<void*> updatedFontPtrs{};
|
||||
// A managed object address can be reused after the original object is collected, and the
|
||||
// replacement Font is dropped by Resources.UnloadUnusedAssets on scene loads. Pair the address
|
||||
// with both InstanceIDs so a recycled address or a reloaded Font is set up again - and so the
|
||||
// map is never cleared wholesale, which would re-initialize every live asset and reshuffle the
|
||||
// atlas under text that is already on screen.
|
||||
struct UpdatedFontState {
|
||||
int fontAssetInstanceId;
|
||||
int sourceFontInstanceId;
|
||||
};
|
||||
std::unordered_map<void*, UpdatedFontState> updatedFontStates{};
|
||||
void UpdateFont(void* TMP_Textself) {
|
||||
if (!Config::replaceFont) return;
|
||||
static auto get_font = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll",
|
||||
@@ -888,8 +1084,39 @@ namespace GakumasLocal::HookMain {
|
||||
|
||||
static auto set_sourceFontFile = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
|
||||
"TMP_FontAsset", "set_sourceFontFile");
|
||||
static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
|
||||
"TMP_FontAsset", "UpdateFontAssetData");
|
||||
static auto set_atlasPopulationMode = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasPopulationMode",
|
||||
{ "TMPro.AtlasPopulationMode" });
|
||||
static auto set_isMultiAtlasTexturesEnabled = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_isMultiAtlasTexturesEnabled",
|
||||
{ "System.Boolean" });
|
||||
static auto ClearFontAssetData = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "ClearFontAssetData",
|
||||
{ "System.Boolean" });
|
||||
static auto get_atlasWidth = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
|
||||
"TMP_FontAsset", "get_atlasWidth");
|
||||
static auto set_atlasWidth = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasWidth",
|
||||
{ "System.Int32" });
|
||||
static auto set_atlasHeight = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasHeight",
|
||||
{ "System.Int32" });
|
||||
static auto get_atlasTextureCount = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro",
|
||||
"TMP_FontAsset", "get_atlasTextureCount");
|
||||
static auto GetInstanceID = Il2cppUtils::GetMethod(
|
||||
"UnityEngine.CoreModule.dll", "UnityEngine", "Object", "GetInstanceID");
|
||||
// TMP's own "this font asset changed, re-layout everything using it" broadcast.
|
||||
static auto ON_FONT_PROPERTY_CHANGED = Il2cppUtils::GetMethod(
|
||||
"Unity.TextMeshPro.dll", "TMPro", "TMPro_EventManager", "ON_FONT_PROPERTY_CHANGED",
|
||||
{ "System.Boolean", "UnityEngine.Object" });
|
||||
|
||||
if (!get_font || !set_font || !get_name || !set_sourceFontFile || !set_atlasPopulationMode ||
|
||||
!set_isMultiAtlasTexturesEnabled || !ClearFontAssetData || !get_atlasWidth ||
|
||||
!set_atlasWidth || !set_atlasHeight || !get_atlasTextureCount || !GetInstanceID ||
|
||||
!ON_FONT_PROPERTY_CHANGED) {
|
||||
Log::Error("UpdateFont disabled: a TMP_FontAsset method could not be resolved");
|
||||
return;
|
||||
}
|
||||
|
||||
auto fontAsset = get_font->Invoke<void*>(TMP_Textself);
|
||||
if (!fontAsset) {
|
||||
@@ -910,13 +1137,61 @@ namespace GakumasLocal::HookMain {
|
||||
auto newFont = GetReplaceFont();
|
||||
if (!newFont) return;
|
||||
|
||||
set_sourceFontFile->Invoke<void>(fontAsset, newFont);
|
||||
if (!updatedFontPtrs.contains(fontAsset)) {
|
||||
updatedFontPtrs.emplace(fontAsset);
|
||||
UpdateFontAssetData->Invoke<void>(fontAsset);
|
||||
const auto fontInstanceId = GetInstanceID->Invoke<int>(fontAsset);
|
||||
const auto sourceFontInstanceId = GetInstanceID->Invoke<int>(newFont);
|
||||
const auto known = updatedFontStates.find(fontAsset);
|
||||
const bool isNewAsset = known == updatedFontStates.end() ||
|
||||
known->second.fontAssetInstanceId != fontInstanceId;
|
||||
const bool sourceFontReloaded = !isNewAsset &&
|
||||
known->second.sourceFontInstanceId != sourceFontInstanceId;
|
||||
if (isNewAsset || sourceFontReloaded) {
|
||||
// Mark the asset before rebuilding it: TMP can re-enter one of the text hooks below
|
||||
// while the rebuild is in progress.
|
||||
updatedFontStates.insert_or_assign(
|
||||
fontAsset, UpdatedFontState{ fontInstanceId, sourceFontInstanceId });
|
||||
set_sourceFontFile->Invoke<void>(fontAsset, newFont);
|
||||
}
|
||||
if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear();
|
||||
|
||||
// Only a genuinely new asset is rebuilt. Resources.UnloadUnusedAssets drops the replacement
|
||||
// Font on most scene loads, and re-clearing on every reload throws away glyphs that text
|
||||
// already on screen is still pointing at - it keeps its stale atlas coordinates and renders
|
||||
// the wrong characters, or nothing at all once the atlas is no longer refilled. The glyphs
|
||||
// already in the atlas came from an identical font file, so a reloaded Font can simply be
|
||||
// re-assigned without touching them.
|
||||
if (isNewAsset) {
|
||||
set_atlasPopulationMode->Invoke<void>(fontAsset, 1); // AtlasPopulationMode.Dynamic
|
||||
set_isMultiAtlasTexturesEnabled->Invoke<void>(fontAsset, true);
|
||||
|
||||
// Glyphs that do not fit on atlas page 0 get a second material, which puts them in
|
||||
// textInfo.meshInfo[1]. The game's stylized text only submits meshInfo[0], so those
|
||||
// characters vanish - correct advance, no quad - while ordinary UI still draws them.
|
||||
// Measured from a frame capture: shelf pitch 79px, ~735 glyphs per 2048x2048 page, and
|
||||
// page 0 was completely full, so the spill was guaranteed rather than occasional.
|
||||
// 4096x4096 holds ~2900 glyphs (16MB of A8); 20 minutes of play reaches about a third.
|
||||
// ponytail: still a ceiling. The atlasTextureCount check below reports when it is hit;
|
||||
// the real fix is making the stylized text submit its sub meshes.
|
||||
if (get_atlasWidth->Invoke<int>(fontAsset) < 4096) {
|
||||
set_atlasWidth->Invoke<void>(fontAsset, 4096);
|
||||
set_atlasHeight->Invoke<void>(fontAsset, 4096);
|
||||
}
|
||||
// Clear, not UpdateFontAssetData: the latter re-adds the asset's entire original
|
||||
// character table first, spending page 0 on glyphs the translation never shows.
|
||||
ClearFontAssetData->Invoke<void>(fontAsset, false);
|
||||
// Clearing invalidates the atlas coordinates baked into every mesh already generated
|
||||
// from this asset. Without this broadcast those texts keep the stale coordinates: the
|
||||
// game's ruby text objects, laid out before the rebuild, showed wrong glyphs when the
|
||||
// old character table was re-added and nothing at all once it was not.
|
||||
ON_FONT_PROPERTY_CHANGED->Invoke<void>(true, fontAsset);
|
||||
}
|
||||
|
||||
static int reportedAtlasPages = 1;
|
||||
const auto atlasPages = get_atlasTextureCount->Invoke<int>(fontAsset);
|
||||
if (atlasPages > reportedAtlasPages) {
|
||||
reportedAtlasPages = atlasPages;
|
||||
Log::ErrorFmt("Font atlas spilled to page %d on [%s] - stylized text will start "
|
||||
"dropping characters",
|
||||
atlasPages - 1, fontAssetName ? fontAssetName->ToString().c_str() : "?");
|
||||
}
|
||||
|
||||
set_font->Invoke<void>(TMP_Textself, fontAsset);
|
||||
|
||||
// auto fontMaterial = get_material->Invoke<void*>(fontAsset);
|
||||
@@ -1224,10 +1499,21 @@ namespace GakumasLocal::HookMain {
|
||||
return ret;
|
||||
}*/
|
||||
|
||||
// 名片编辑页的 P 偶像页签用 UserIdolCardSkinList.Exists(skin.Id) 过滤“拥有的皮肤”,随后按底卡去
|
||||
// UserIdolCardList 查 UserIdolCard 取 IllustrationStage。下面 Exists 对所有 i_card-skin-* 返回 true 会把
|
||||
// 没有底卡的皮肤也放进去,查到 null 直接 NRE,整个界面初始化中断(表现为卡死)。该模型构造期间放行真实结果。
|
||||
static bool meishiModelConstructing = false;
|
||||
DEFINE_HOOK(void, MeishiBasePIdolTabPageModel_ctor, (void* self, void* mtd)) {
|
||||
meishiModelConstructing = true;
|
||||
MeishiBasePIdolTabPageModel_ctor_Orig(self, mtd);
|
||||
meishiModelConstructing = false;
|
||||
}
|
||||
|
||||
DEFINE_HOOK(bool, UserIdolCardSkinCollection_Exists, (void* self, Il2cppString* id, void* mtd)) { // Live默认选择
|
||||
auto ret = UserIdolCardSkinCollection_Exists_Orig(self, id, mtd);
|
||||
// Log::DebugFmt("UserIdolCardSkinCollection_Exists: %s, ret: %d", id->ToString().c_str(), ret);
|
||||
if (!(Config::dbgMode && Config::unlockAllLive)) return ret;
|
||||
if (meishiModelConstructing) return ret;
|
||||
|
||||
if (id) {
|
||||
std::string idStr = id->ToString();
|
||||
@@ -1724,6 +2010,75 @@ namespace GakumasLocal::HookMain {
|
||||
return AddPhotographyIdolSkinItems(ret, characterId ? characterId->ToString() : "", getItem, getId);
|
||||
}
|
||||
|
||||
// ---- 技能卡皮肤(ProduceCardSkin):没有专属皮也播专属皮的演出 ----
|
||||
// 考试内所有皮肤表现都由 ExamCardData.ProduceCardSkinId 派生:
|
||||
// PrePlayCardAsync -> PlayCutInAsync(skinId) -> TryGetCutInParams -> mov_{id}_skillcard.usm + cutInType
|
||||
// ExamCardButtonImage.SetCard -> GetThumbnailLoopMovieName(skinId) -> 手牌循环缩略图
|
||||
// 所以在这个 getter 上,把「没挂皮肤」的卡补成主表里同 ProduceCardId、CutInType 最高的专属皮即可。
|
||||
// 注意:该 getter 另有 ExamDataSerializeData / ExamParameterModel 两个读取方,
|
||||
// 伪造的 id 会进考试状态序列化和 play log(竞赛回放会上传),纯本地表现之外的副作用仅此。
|
||||
std::unordered_map<std::string, std::string> produceCardSkinCutInRemap{}; // produceCardId -> 专属皮 id
|
||||
bool produceCardSkinCutInRemapBuilt = false;
|
||||
|
||||
void BuildProduceCardSkinCutInRemap() {
|
||||
if (produceCardSkinCutInRemapBuilt) return;
|
||||
static auto getMaster = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "MasterManager", "get_ProduceCardSkinMaster");
|
||||
static auto masterKlass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Master", "ProduceCardSkinMaster");
|
||||
static auto getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "ProduceCardSkin", "get_Id");
|
||||
static auto getCardId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "ProduceCardSkin", "get_ProduceCardId");
|
||||
static auto getCutInType = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "ProduceCardSkin", "get_CutInType");
|
||||
if (!getMaster || !masterKlass || !getId || !getCardId || !getCutInType) return;
|
||||
// 取 1 参重载 GetAllWithSortByKey(sortType),末尾补隐藏 MethodInfo*
|
||||
static auto getAll_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(masterKlass->address, "GetAllWithSortByKey", 1);
|
||||
if (!getAll_mtd) return;
|
||||
auto getAll = reinterpret_cast<UnityResolve::UnityType::List<void*>* (*)(void*, int, void*)>(getAll_mtd->methodPointer);
|
||||
|
||||
auto master = getMaster->Invoke<void*>(nullptr);
|
||||
if (!master) return;
|
||||
auto list = getAll(master, 0, (void*)getAll_mtd);
|
||||
if (!list) return;
|
||||
|
||||
// produceCardId -> (最高 cutInType, 皮肤 id)。没有 cut-in 的皮抄过来也没演出,直接跳过
|
||||
std::unordered_map<std::string, std::pair<int, std::string>> best{};
|
||||
int valid = 0, withCutIn = 0;
|
||||
for (auto item : list->ToArray()->ToVector()) {
|
||||
if (!item) continue; // ToVector 含 List 容量内的 null 槽位
|
||||
auto id = getId->Invoke<Il2cppString*>(item);
|
||||
auto cardId = getCardId->Invoke<Il2cppString*>(item);
|
||||
if (!id || !cardId) continue;
|
||||
++valid;
|
||||
// ProduceCardSkinCutInType: Unknown=0 / Small=1 / Mid=2 / Grand=3
|
||||
const int cutIn = getCutInType->Invoke<int>(item);
|
||||
if (cutIn <= 0) continue;
|
||||
++withCutIn;
|
||||
auto& slot = best[cardId->ToString()];
|
||||
if (cutIn > slot.first) slot = { cutIn, id->ToString() };
|
||||
}
|
||||
if (!valid) return; // 主表还没加载,下次再建
|
||||
|
||||
for (const auto& [cardId, slot] : best) produceCardSkinCutInRemap.emplace(cardId, slot.second);
|
||||
produceCardSkinCutInRemapBuilt = true;
|
||||
Log::InfoFmt("ProduceCardSkin cut-in remap built: %zu cards (rows=%d, withCutIn=%d)",
|
||||
produceCardSkinCutInRemap.size(), valid, withCutIn);
|
||||
}
|
||||
|
||||
// 卡本身没挂皮肤时,按 produceCardId 补一个带 cut-in 的皮肤 id;
|
||||
// 已经挂了皮肤的走原样,不覆盖玩家自己的选择
|
||||
DEFINE_HOOK(Il2cppString*, ExamCardData_get_ProduceCardSkinId, (void* self, void* mtd)) {
|
||||
auto ret = ExamCardData_get_ProduceCardSkinId_Orig(self, mtd);
|
||||
if (!Config::dbgMode) return ret;
|
||||
if (ret && !ret->ToString().empty()) return ret;
|
||||
|
||||
static auto ExamCardData_get_Id = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.InGame.Card", "ExamCardData", "get_Id");
|
||||
if (!ExamCardData_get_Id) return ret;
|
||||
auto cardId = ExamCardData_get_Id->Invoke<Il2cppString*>(self);
|
||||
if (!cardId) return ret;
|
||||
|
||||
BuildProduceCardSkinCutInRemap();
|
||||
auto it = produceCardSkinCutInRemap.find(cardId->ToString());
|
||||
return it == produceCardSkinCutInRemap.end() ? ret : Il2cppString::New(it->second);
|
||||
}
|
||||
|
||||
void* getCompletedUniTask() {
|
||||
static auto unitask_klass = Il2cppUtils::GetClass("UniTask.dll", "Cysharp.Threading.Tasks", "UniTask");
|
||||
static auto CompletedTask_field = unitask_klass->Get<UnityResolve::Field>("CompletedTask");
|
||||
@@ -2480,6 +2835,11 @@ namespace GakumasLocal::HookMain {
|
||||
"ExchangeSpToNormal",
|
||||
{ "Campus.Common.Proto.Client.Enums.ProduceStepType" }));
|
||||
|
||||
ADD_HOOK(ExamCardData_get_ProduceCardSkinId,
|
||||
Il2cppUtils::GetMethodPointer(
|
||||
"Assembly-CSharp.dll", "Campus.InGame.Card", "ExamCardData",
|
||||
"get_ProduceCardSkinId"));
|
||||
|
||||
/* // 此 block 为 MasterBase 相关的 hook,后来发现它们最后都会调用 MessageExtensions.MergeFrom 进行构造,遂停用。现留档以备用
|
||||
// ADD_HOOK(MasterBase_GetAll, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master",
|
||||
// "MasterBase`2", "GetAll", {"*", "*", "*", "*", "*"}));
|
||||
@@ -2533,6 +2893,8 @@ namespace GakumasLocal::HookMain {
|
||||
if (UserIdolCardSkinCollection_Exists_mtd) {
|
||||
ADD_HOOK(UserIdolCardSkinCollection_Exists, UserIdolCardSkinCollection_Exists_mtd->methodPointer);
|
||||
}
|
||||
ADD_HOOK(MeishiBasePIdolTabPageModel_ctor, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.OutGame",
|
||||
"MeishiBasePIdolTabPageModel", ".ctor"));
|
||||
|
||||
auto UserCostumeCollection_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User",
|
||||
"UserCostumeCollection");
|
||||
@@ -2728,6 +3090,18 @@ 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(CampusGyroController_ctor, Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common",
|
||||
"CampusGyroController", ".ctor"));
|
||||
ADD_HOOK(PhotographyActivityController_Initialize, Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Photography",
|
||||
"PhotographyActivityController", "Initialize"));
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user