diff --git a/app/build.gradle b/app/build.gradle index 7044d47..6ea5813 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { minSdk 29 targetSdk 34 versionCode 12 - versionName "v3.4.0" + versionName "v3.4.1" buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -132,4 +132,4 @@ dependencies { implementation(libs.shadowhook) compileOnly(libs.xposed.api) implementation(libs.kotlinx.serialization.json) -} \ No newline at end of file +} diff --git a/app/src/main/cpp/GakumasLocalify/Hook.cpp b/app/src/main/cpp/GakumasLocalify/Hook.cpp index 4fab861..7d2e294 100644 --- a/app/src/main/cpp/GakumasLocalify/Hook.cpp +++ b/app/src/main/cpp/GakumasLocalify/Hook.cpp @@ -7,7 +7,13 @@ #include "Local.h" #include "MasterLocal.h" #include +#include #include +#include +#include +#include +#include +#include #include "camera/camera.hpp" #include "config/Config.hpp" // #include @@ -116,14 +122,30 @@ namespace GakumasLocal::HookMain { UnityResolve::UnityType::Camera* mainCameraCache = nullptr; UnityResolve::UnityType::Transform* cameraTransformCache = nullptr; - void CheckAndUpdateMainCamera() { + + void CheckAndUpdateMainCamera(UnityResolve::UnityType::Camera* fallbackCamera = nullptr) { if (!Config::enableFreeCamera) return; if (IsNativeObjectAlive(mainCameraCache) && IsNativeObjectAlive(cameraTransformCache)) return; - mainCameraCache = UnityResolve::UnityType::Camera::GetMain(); - cameraTransformCache = mainCameraCache->GetTransform(); + // 优先使用游戏传入的真实相机(渲染回调 / FOV hook 捕获)。 + // 注意:不要用 UnityResolve 的 managed Invoke 直接调 Camera.get_main / get_transform, + // Unity 6 下该调用缺少 MethodInfo* 参数,可能返回垃圾指针(崩溃根因)。 + if (!mainCameraCache || !IsNativeObjectAlive(mainCameraCache)) { + mainCameraCache = fallbackCamera; + } + // 不再回退到 Camera.GetMain()/GetCurrent():UnityResolve 的 managed Invoke 可能返回垃圾指针。 + // 相机只从游戏自己的调用捕获(EndCameraRendering 参数 / FOV hook / get_main hook)。 + // cameraTransformCache 由 Component.get_transform hook 在游戏调用时捕获。 + + if (!mainCameraCache) { + cameraTransformCache = nullptr; + } } + bool TryLookRotationQuat(const UnityResolve::UnityType::Vector3& forwardIn, + const UnityResolve::UnityType::Vector3& upIn, + UnityResolve::UnityType::Quaternion* outQuat); + Il2cppUtils::Resolution_t GetResolution() { static auto GetResolution = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine", "Screen", "get_currentResolution"); @@ -144,6 +166,12 @@ namespace GakumasLocal::HookMain { DEFINE_HOOK(void, Unity_set_fieldOfView, (UnityResolve::UnityType::Camera* self, float value)) { if (Config::enableFreeCamera) { + // self 是游戏传的真实相机,直接捕获。 + // Cinemachine 每帧会把镜头参数应用到输出相机,这里拿到的就是实际游戏相机。 + if (self != mainCameraCache) { + mainCameraCache = self; + cameraTransformCache = nullptr; // 相机变了,Transform 需要重新解析 + } if (self == mainCameraCache) { value = GKCamera::baseCamera.fov; } @@ -153,6 +181,10 @@ namespace GakumasLocal::HookMain { DEFINE_HOOK(float, Unity_get_fieldOfView, (UnityResolve::UnityType::Camera* self)) { if (Config::enableFreeCamera) { + if (self != mainCameraCache) { + mainCameraCache = self; + cameraTransformCache = nullptr; + } if (self == mainCameraCache) { static auto get_orthographic = reinterpret_cast(Il2cppUtils::il2cpp_resolve_icall( "UnityEngine.Camera::get_orthographic()" @@ -162,7 +194,9 @@ namespace GakumasLocal::HookMain { )); for (const auto& i : UnityResolve::UnityType::Camera::GetAllCamera()) { - // Log::DebugFmt("get_orthographic: %d", get_orthographic(i)); + if (get_orthographic) { + // Log::DebugFmt("get_orthographic: %d", get_orthographic(i)); + } // set_orthographic(i, false); Unity_set_fieldOfView_Orig(i, GKCamera::baseCamera.fov); } @@ -175,21 +209,159 @@ namespace GakumasLocal::HookMain { return Unity_get_fieldOfView_Orig(self); } + // 从游戏自己的调用里捕获真实对象指针: + // Camera.get_main / Component.get_transform 是带 MethodInfo* 尾参的 managed 方法, + // 用 hook(而不是 UnityResolve 的 managed Invoke)才能拿到可靠结果。 + DEFINE_HOOK(UnityResolve::UnityType::Camera*, Camera_get_main, (void* mtd)) { + auto ret = Camera_get_main_Orig(mtd); + if (Config::enableFreeCamera && ret && ret != mainCameraCache) { + mainCameraCache = ret; + cameraTransformCache = nullptr; + } + return ret; + } + + DEFINE_HOOK(UnityResolve::UnityType::Transform*, Component_get_transform, (void* self, void* mtd)) { + auto ret = Component_get_transform_Orig(self, mtd); + if (Config::enableFreeCamera && mainCameraCache && self == mainCameraCache) { + cameraTransformCache = ret; + } + return ret; + } + UnityResolve::UnityType::Transform* cacheTrans = nullptr; UnityResolve::UnityType::Quaternion cacheRotation{}; UnityResolve::UnityType::Vector3 cachePosition{}; UnityResolve::UnityType::Vector3 cacheForward{}; UnityResolve::UnityType::Vector3 cacheLookAt{}; + // 计算当前模式下的相机位置/朝向。返回 false 表示暂不可用(NaN/目标丢失)。 + bool ComputeFreeCameraPose(UnityResolve::UnityType::Vector3* pos, UnityResolve::UnityType::Quaternion* rot) { + using Vector3 = UnityResolve::UnityType::Vector3; + using Quaternion = UnityResolve::UnityType::Quaternion; + + Vector3 lookAt{}; + switch (GKCamera::GetCameraMode()) { + case GKCamera::CameraMode::FREE: { + *pos = GKCamera::baseCamera.GetPos(); + lookAt = GKCamera::baseCamera.GetLookAt(); + } break; + case GKCamera::CameraMode::FIRST_PERSON: { + if (!cacheTrans || !IsNativeObjectAlive(cacheTrans)) return false; + *pos = GKCamera::CalcFirstPersonPosition(cachePosition, cacheForward, GKCamera::firstPersonPosOffset); + lookAt = cacheLookAt; + } break; + case GKCamera::CameraMode::FOLLOW: { + lookAt = GKCamera::CalcFollowModeLookAt(cachePosition, GKCamera::followPosOffset); + *pos = GKCamera::CalcPositionFromLookAt(lookAt, GKCamera::followPosOffset); + } break; + default: + return false; + } + + if (!std::isfinite(pos->x) || !std::isfinite(pos->y) || !std::isfinite(pos->z) || + !std::isfinite(lookAt.x) || !std::isfinite(lookAt.y) || !std::isfinite(lookAt.z)) { + return false; + } + + const auto forward = Vector3(lookAt.x - pos->x, lookAt.y - pos->y, lookAt.z - pos->z); + return TryLookRotationQuat(forward, Vector3(0, 1, 0), rot); + } + + bool IsCameraControlTransform(void* self) { + if (!self || !cameraTransformCache) return false; + return self == cameraTransformCache; + } + + // Cinemachine 把最终相机状态写入 Unity 相机的唯一入口(CinemachineBrain.PushStateToUnityCamera)。 + // CameraState 是大结构体,按值传递时经隐藏指针传入(hook 的 state 参数即指向该副本), + // 直接改副本,游戏会用自己内部的原生路径应用——不需要调用任何 Transform API,绝对安全。 + // 字段偏移来自 hook-reference dump.cs: + // 0x00 Lens.FieldOfView | 0x48 RawPosition | 0x54 RawOrientation + // 0x64 PositionDampingBypass | 0x74 PositionCorrection | 0x80 OrientationCorrection + DEFINE_HOOK(void, CinemachineBrain_PushStateToUnityCamera, (void* self, void* state, void* mtd)) { + if (Config::enableFreeCamera && state) { + CheckAndUpdateMainCamera(); + + using Vector3 = UnityResolve::UnityType::Vector3; + using Quaternion = UnityResolve::UnityType::Quaternion; + auto* rawPos = reinterpret_cast(static_cast(state) + 0x48); + auto* rawRot = reinterpret_cast(static_cast(state) + 0x54); + auto* damping = reinterpret_cast(static_cast(state) + 0x64); + auto* posCorr = reinterpret_cast(static_cast(state) + 0x74); + auto* rotCorr = reinterpret_cast(static_cast(state) + 0x80); + auto* fov = reinterpret_cast(static_cast(state) + 0x00); + + if (ComputeFreeCameraPose(rawPos, rawRot)) { + *damping = Vector3(0, 0, 0); + *posCorr = Vector3(0, 0, 0); + *rotCorr = Quaternion(0, 0, 0, 1); + *fov = GKCamera::baseCamera.fov; + } + } + + return CinemachineBrain_PushStateToUnityCamera_Orig(self, state, mtd); + } + + // Cinemachine 3(Unity 6)用 SetPositionAndRotation 一次性写相机位置+旋转, + // set_position/set_rotation 拦截不到相机,必须在这里接管。 + DEFINE_HOOK(void, Unity_SetPositionAndRotation_Injected, (UnityResolve::UnityType::Transform* self, + UnityResolve::UnityType::Vector3* position, + UnityResolve::UnityType::Quaternion* rotation)) { + if (Config::enableFreeCamera) { + CheckAndUpdateMainCamera(); + + const auto isControl = IsCameraControlTransform(self); + if (isControl) { + UnityResolve::UnityType::Vector3 pos{}; + UnityResolve::UnityType::Quaternion rot{}; + if (ComputeFreeCameraPose(&pos, &rot)) { + *position = pos; + *rotation = rot; + } + } + } + + return Unity_SetPositionAndRotation_Injected_Orig(self, position, rotation); + } + + // 兜底:游戏若用局部坐标写相机(rig 挂载场景),位置同样接管。 + DEFINE_HOOK(void, Unity_set_localPosition_Injected, (UnityResolve::UnityType::Transform* self, + UnityResolve::UnityType::Vector3* data)) { + if (Config::enableFreeCamera) { + CheckAndUpdateMainCamera(); + + const auto isControl = IsCameraControlTransform(self); + if (isControl) { + const auto cameraMode = GKCamera::GetCameraMode(); + if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) { + if (cacheTrans && IsNativeObjectAlive(cacheTrans)) { + *data = GKCamera::CalcFirstPersonPosition(cachePosition, cacheForward, GKCamera::firstPersonPosOffset); + } + } + else if (cameraMode == GKCamera::CameraMode::FOLLOW) { + auto newLookAtPos = GKCamera::CalcFollowModeLookAt(cachePosition, GKCamera::followPosOffset); + auto pos = GKCamera::CalcPositionFromLookAt(newLookAtPos, GKCamera::followPosOffset); + data->x = pos.x; + data->y = pos.y; + data->z = pos.z; + } + else { + auto& origCameraPos = GKCamera::baseCamera.pos; + data->x = origCameraPos.x; + data->y = origCameraPos.y; + data->z = origCameraPos.z; + } + } + } + + return Unity_set_localPosition_Injected_Orig(self, data); + } + DEFINE_HOOK(void, Unity_set_rotation_Injected, (UnityResolve::UnityType::Transform* self, UnityResolve::UnityType::Quaternion* value)) { if (Config::enableFreeCamera) { - 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); - - if (cameraTransformCache == self) { + const auto isControl = IsCameraControlTransform(self); + if (isControl) { const auto cameraMode = GKCamera::GetCameraMode(); if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) { if (cacheTrans && IsNativeObjectAlive(cacheTrans)) { @@ -200,22 +372,39 @@ namespace GakumasLocal::HookMain { static GakumasLocal::Misc::FixedSizeQueue recordsY(60); const auto newY = GKCamera::CheckNewY(cacheLookAt, true, recordsY); UnityResolve::UnityType::Vector3 newCacheLookAt{cacheLookAt.x, newY, cacheLookAt.z}; - lookat_injected(self, &newCacheLookAt, &worldUp); - return; + const auto pos = GKCamera::CalcFirstPersonPosition( + cachePosition, cacheForward, GKCamera::firstPersonPosOffset); + UnityResolve::UnityType::Quaternion q{}; + const auto forward = UnityResolve::UnityType::Vector3( + newCacheLookAt.x - pos.x, newCacheLookAt.y - pos.y, newCacheLookAt.z - pos.z); + if (TryLookRotationQuat(forward, UnityResolve::UnityType::Vector3(0, 1, 0), &q)) { + *value = q; + } } } } else if (cameraMode == GKCamera::CameraMode::FOLLOW) { auto newLookAtPos = GKCamera::CalcFollowModeLookAt(cachePosition, GKCamera::followPosOffset, true); - lookat_injected(self, &newLookAtPos, &worldUp); - return; + const auto pos = GKCamera::CalcPositionFromLookAt(newLookAtPos, GKCamera::followPosOffset); + UnityResolve::UnityType::Quaternion q{}; + const auto forward = UnityResolve::UnityType::Vector3( + newLookAtPos.x - pos.x, newLookAtPos.y - pos.y, newLookAtPos.z - pos.z); + if (TryLookRotationQuat(forward, UnityResolve::UnityType::Vector3(0, 1, 0), &q)) { + *value = q; + } } else { auto& origCameraLookat = GKCamera::baseCamera.lookAt; - lookat_injected(self, &origCameraLookat, &worldUp); - // Log::DebugFmt("fov: %f, target: %f", Unity_get_fieldOfView_Orig(mainCameraCache), GKCamera::baseCamera.fov); - return; + auto& origCameraPos = GKCamera::baseCamera.pos; + UnityResolve::UnityType::Quaternion q{}; + const auto forward = UnityResolve::UnityType::Vector3( + origCameraLookat.x - origCameraPos.x, + origCameraLookat.y - origCameraPos.y, + origCameraLookat.z - origCameraPos.z); + if (TryLookRotationQuat(forward, UnityResolve::UnityType::Vector3(0, 1, 0), &q)) { + *value = q; + } } } } @@ -226,7 +415,8 @@ namespace GakumasLocal::HookMain { if (Config::enableFreeCamera) { CheckAndUpdateMainCamera(); - if (cameraTransformCache == self) { + const auto isControl = IsCameraControlTransform(self); + if (isControl) { const auto cameraMode = GKCamera::GetCameraMode(); if (cameraMode == GKCamera::CameraMode::FIRST_PERSON) { if (cacheTrans && IsNativeObjectAlive(cacheTrans)) { @@ -254,6 +444,64 @@ namespace GakumasLocal::HookMain { return Unity_set_position_Injected_Orig(self, data); } + // 用 forward/up 构造 Unity 兼容的四元数(等价 Quaternion.LookRotation)。 + // 返回 false 表示 forward 与 up 平行(垂直看天/看地),此时不应写入旋转,保持当前朝向。 + bool TryLookRotationQuat(const UnityResolve::UnityType::Vector3& forwardIn, + const UnityResolve::UnityType::Vector3& upIn, + UnityResolve::UnityType::Quaternion* outQuat) { + using Vector3 = UnityResolve::UnityType::Vector3; + using Quaternion = UnityResolve::UnityType::Quaternion; + + Vector3 forward = forwardIn; + const auto fwdLenSq = forward.x * forward.x + forward.y * forward.y + forward.z * forward.z; + if (fwdLenSq < 1e-8f) { + return false; + } + const auto invFwdLen = 1.0f / std::sqrt(fwdLenSq); + forward = Vector3(forward.x * invFwdLen, forward.y * invFwdLen, forward.z * invFwdLen); + + Vector3 right = Vector3( + upIn.y * forward.z - upIn.z * forward.y, + upIn.z * forward.x - upIn.x * forward.z, + upIn.x * forward.y - upIn.y * forward.x); + const auto rightLenSq = right.x * right.x + right.y * right.y + right.z * right.z; + if (rightLenSq < 1e-8f) { + return false; + } + const auto invRightLen = 1.0f / std::sqrt(rightLenSq); + right = Vector3(right.x * invRightLen, right.y * invRightLen, right.z * invRightLen); + + const Vector3 up = Vector3( + forward.y * right.z - forward.z * right.y, + forward.z * right.x - forward.x * right.z, + forward.x * right.y - forward.y * right.x); + + // 旋转矩阵列向量(right/up/forward)-> 标准矩阵转四元数 + const float m00 = right.x, m01 = up.x, m02 = forward.x; + const float m10 = right.y, m11 = up.y, m12 = forward.y; + const float m20 = right.z, m21 = up.z, m22 = forward.z; + + const float tr = m00 + m11 + m22; + if (tr > 0.0f) { + const float s = std::sqrt(tr + 1.0f) * 2.0f; + *outQuat = Quaternion((m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25f * s); + return true; + } + if (m00 > m11 && m00 > m22) { + const float s = std::sqrt(1.0f + m00 - m11 - m22) * 2.0f; + *outQuat = Quaternion(0.25f * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s); + return true; + } + if (m11 > m22) { + const float s = std::sqrt(1.0f + m11 - m00 - m22) * 2.0f; + *outQuat = Quaternion((m01 + m10) / s, 0.25f * s, (m12 + m21) / s, (m02 - m20) / s); + return true; + } + const float s = std::sqrt(1.0f + m22 - m00 - m11) * 2.0f; + *outQuat = Quaternion((m02 + m20) / s, (m12 + m21) / s, 0.25f * s, (m10 - m01) / s); + return true; + } + #ifdef GKMS_WINDOWS DEFINE_HOOK(void*, InternalSetOrientationAsync, (void* retstr, void* self, int type, void* c, void* tc, void* mtd)) { switch (Config::gameOrientation) { @@ -277,10 +525,16 @@ namespace GakumasLocal::HookMain { DEFINE_HOOK(void, EndCameraRendering, (void* ctx, void* camera, void* method)) { EndCameraRendering_Orig(ctx, camera, method); - if (Config::enableFreeCamera && mainCameraCache) { - Unity_set_fieldOfView_Orig(mainCameraCache, GKCamera::baseCamera.fov); - if (GKCamera::GetCameraMode() == GKCamera::CameraMode::FIRST_PERSON) { - mainCameraCache->SetNearClipPlane(0.001f); + if (Config::enableFreeCamera) { + // Camera.main 可能拿不到 3.0.0 实际渲染相机,用当前正在渲染的相机兜底 + CheckAndUpdateMainCamera(static_cast(camera)); + if (mainCameraCache && IsNativeObjectAlive(mainCameraCache)) { + // 注意:不能在渲染回调里写相机 Transform(UnityPlayer 会崩), + // 位置/朝向由独立驱动线程负责,这里只保留 FOV/近裁剪等属性设置。 + Unity_set_fieldOfView_Orig(mainCameraCache, GKCamera::baseCamera.fov); + if (GKCamera::GetCameraMode() == GKCamera::CameraMode::FIRST_PERSON) { + mainCameraCache->SetNearClipPlane(0.001f); + } } } } @@ -849,6 +1103,12 @@ namespace GakumasLocal::HookMain { EffectGroup_ctor_Orig(self, mtd); } + // 原样返回传入的 ProduceStepType,阻止 SP 被转换成 Normal,日程上才看得到 SP。 + // 不能调 _Orig,调了就是原版行为,SP 会被抹平。 + DEFINE_HOOK(int, ExamExtensions_ExchangeSpToNormal, (int type, void* mtd)) { + return Config::dbgMode ? type : ExamExtensions_ExchangeSpToNormal_Orig(type, mtd); + } + // 用于本地化 MasterDB DEFINE_HOOK(void, MessageExtensions_MergeFrom, (void* message, void* span, void* mtd)) { MessageExtensions_MergeFrom_Orig(message, span, mtd); @@ -1139,55 +1399,331 @@ namespace GakumasLocal::HookMain { return origList; } - DEFINE_HOOK(void*, UserCostumeCollection_FindBy, (void* self, void* predicate, void* mtd)) { - auto ret = UserCostumeCollection_FindBy_Orig(self, predicate, mtd); - if (!Config::unlockAllLiveCostume) return ret; - - auto this_klass = Il2cppUtils::get_class_from_instance(self); - // auto predicate_klass = Il2cppUtils::get_class_from_instance(predicate); // System::Predicate`1 - // Log::DebugFmt("UserCostumeCollection_FindBy this: %s::%s, predicate: %s::%s", this_klass->namespaze, this_klass->name, - // predicate_klass->namespaze, predicate_klass->name); - - static auto UserCostumeCollection_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.User", - "UserCostumeCollection"); - static auto UserCostumeCollection_GetAllList_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( - UserCostumeCollection_klass->address, "GetAllList", 1); - static auto UserCostumeCollection_GetAllList = reinterpret_cast(UserCostumeCollection_GetAllList_mtd->methodPointer); - - std::string thisKlassName(this_klass->name); - // Campus.Common.User::UserCostumeHeadCollection || Campus.Common.User::UserCostumeCollection - // 两个 class 的 GetAllList 均使用的父类 Qua.UserDataManagement.UserDataCollectionBase`2 的方法,地址一致 - if (thisKlassName == "UserCostumeHeadCollection") { + // 把主表全部服装/头部 ID 补进用户服装列表;klassName 决定按 body 还是 head 处理 + void* AugmentUserCostumeList(const std::string& klassName, void* origList) { + if (klassName == "UserCostumeHeadCollection") { static auto UserCostume_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "Clone"); static auto UserCostume_get_CostumeHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "get_CostumeHeadId"); static auto UserCostume_set_CostumeHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "set_CostumeHeadId"); - // auto ret_klass = Il2cppUtils::get_class_from_instance(ret); // WhereEnumerableIterator - auto origList = UserCostumeCollection_GetAllList(self, nullptr); + // 游戏改名/改版本时这些会是 null,直接返回原列表,别带着空指针往下走 + if (!UserCostume_Clone || !UserCostume_get_CostumeHeadId || !UserCostume_set_CostumeHeadId) return origList; auto allIds = GetIdolMusicIdAll("", GetIdolIdType::CostumeHeadId); // List return AddIdsToUserDataCollectionFromMaster(origList, allIds, UserCostume_get_CostumeHeadId, UserCostume_set_CostumeHeadId, UserCostume_Clone); } - else if (thisKlassName == "UserCostumeCollection") { - // static auto UserCostume_klass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume"); + if (klassName == "UserCostumeCollection") { static auto UserCostume_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "Clone"); static auto UserCostume_get_CostumeId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "get_CostumeId"); static auto UserCostume_set_CostumeId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "set_CostumeId"); - // auto ret_klass = Il2cppUtils::get_class_from_instance(ret); // WhereEnumerableIterator - auto origList = UserCostumeCollection_GetAllList(self, nullptr); + if (!UserCostume_Clone || !UserCostume_get_CostumeId || !UserCostume_set_CostumeId) return origList; auto allIds = GetIdolMusicIdAll("", GetIdolIdType::CostumeId); // List return AddIdsToUserDataCollectionFromMaster(origList, allIds, UserCostume_get_CostumeId, UserCostume_set_CostumeId, UserCostume_Clone); } + return origList; + } + + DEFINE_HOOK(void*, UserCostumeCollection_FindBy, (void* self, void* predicate, void* mtd)) { + auto ret = UserCostumeCollection_FindBy_Orig(self, predicate, mtd); + if (!(Config::dbgMode && Config::unlockAllLiveCostume)) return ret; + + auto this_klass = Il2cppUtils::get_class_from_instance(self); + + std::string thisKlassName(this_klass->name); + // Campus.Common.User::UserCostumeHeadCollection || Campus.Common.User::UserCostumeCollection + // 两个 class 的 GetAllList 均使用的父类 Qua.UserDataManagement.UserDataCollectionBase`2 的方法,地址一致 + if (thisKlassName == "UserCostumeHeadCollection" || thisKlassName == "UserCostumeCollection") { + auto getAllListMtd = Il2cppUtils::il2cpp_class_get_method_from_name(this_klass, "GetAllList", 1); + if (!getAllListMtd) return ret; + auto getAllList = reinterpret_cast(getAllListMtd->methodPointer); + auto origList = getAllList(self, nullptr, (void*)getAllListMtd); + return AugmentUserCostumeList(thisKlassName, origList); + } return ret; } + // CostumePhotoGroup 主表收录的全部服装 ID——游戏自己的“可拍摄(已实装)”白名单 + std::unordered_set GetPhotoGroupCostumeIds() { + std::unordered_set ret{}; + auto getMaster = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "MasterManager", "get_CostumePhotoGroupMaster"); + auto masterKlass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Master", "CostumePhotoGroupMaster"); + auto getCostumeIds = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "CostumePhotoGroup", "get_CostumeIds"); + if (!getMaster || !masterKlass || !getCostumeIds) return ret; + auto getAll_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(masterKlass->address, "GetAllWithSortByKey", 1); + if (!getAll_mtd) return ret; + auto getAll = reinterpret_cast* (*)(void*, int, void*)>(getAll_mtd->methodPointer); + + auto master = getMaster->Invoke(nullptr); + if (!master) return ret; + auto list = getAll(master, 0, (void*)getAll_mtd); + if (!list) return ret; + for (auto group : list->ToArray()->ToVector()) { + if (!group) continue; // ToVector 含 List 容量内的 null 槽位 + auto rep = getCostumeIds->Invoke(group); // RepeatedField + if (!rep) continue; + auto rep_klass = Il2cppUtils::get_class_from_instance(rep); + static auto count_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(rep_klass, "get_Count", 0); + static auto item_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(rep_klass, "get_Item", 1); + if (!count_mtd || !item_mtd) continue; + // 共享泛型:末尾传 MethodInfo* + auto getCount = reinterpret_cast(count_mtd->methodPointer); + auto getItem = reinterpret_cast(item_mtd->methodPointer); + int n = getCount(rep, (void*)count_mtd); + for (int i = 0; i < n; ++i) { + auto s = getItem(rep, i, (void*)item_mtd); + if (s) ret.emplace(s->ToString()); + } + } + return ret; + } + + // 主表补充 ID:异色 = 与基础服装(baseIds,IdolCardSkin 来源)共享 colorGroupId 的变体; + // 另加 CostumePhotoGroup 白名单条目;ViewStartTime 在未来的未实装条目跳过。 + // isHead 时返回这些服装引用的头部 ID + std::vector GetMasterCostumeIdsAll(bool isHead, const std::unordered_set& baseIds) { + std::vector ret{}; + auto getMaster = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "MasterManager", "get_CostumeMaster"); + auto masterKlass = Il2cppUtils::GetClass("Assembly-CSharp.dll", "Campus.Common.Master", "CostumeMaster"); + auto getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_Id"); + auto getHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_CostumeHeadId"); + auto getDefaultHeadId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_DefaultCostumeHeadId"); + auto getColorGroupId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_CostumeColorGroupId"); + auto getViewStartTime = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_ViewStartTime"); + if (!getMaster || !masterKlass || !getId || !getHeadId || !getDefaultHeadId || !getColorGroupId || !getViewStartTime) return ret; + // 取 1 参重载 GetAllWithSortByKey(sortType),末尾补隐藏 MethodInfo* + auto getAll_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(masterKlass->address, "GetAllWithSortByKey", 1); + if (!getAll_mtd) return ret; + auto getAll = reinterpret_cast* (*)(void*, int, void*)>(getAll_mtd->methodPointer); + + auto photoAllowed = GetPhotoGroupCostumeIds(); + if (photoAllowed.empty()) return ret; + + auto master = getMaster->Invoke(nullptr); + if (!master) return ret; + auto list = getAll(master, 0, (void*)getAll_mtd); + if (!list) return ret; + auto items = list->ToArray()->ToVector(); + + // 第一遍:基础服装(IdolCardSkin 来源)与拍摄白名单条目的颜色组 → 允许其全部异色变体 + std::unordered_set allowedColorGroups{}; + for (auto item : items) { + if (!item) continue; // ToVector 含 List 容量内的 null 槽位 + auto id = getId->Invoke(item); + if (!id) continue; + auto idStr = id->ToString(); + if (!photoAllowed.contains(idStr) && !baseIds.contains(idStr)) continue; + auto cg = getColorGroupId->Invoke(item); + if (cg && !cg->ToString().empty()) allowedColorGroups.emplace(cg->ToString()); + } + + const auto nowSec = (int64_t)time(nullptr); + std::unordered_set seen{}; + int skippedFuture = 0; + for (auto item : items) { + if (!item) continue; + auto id = getId->Invoke(item); + if (!id) continue; + bool allowed = photoAllowed.contains(id->ToString()); + auto viewStart = getViewStartTime->Invoke(item); + if (viewStart > 4000000000LL) viewStart /= 1000; // 毫秒 → 秒 + + if (!allowed) { + auto cg = getColorGroupId->Invoke(item); + allowed = cg && !cg->ToString().empty() && allowedColorGroups.contains(cg->ToString()); + } + // 独立活动/商店服(如水手服泳装):无颜色组但有已到期的公开时间 + if (!allowed) allowed = viewStart > 0 && viewStart <= nowSec; + if (!allowed) continue; + + // 未实装(公开时间在未来)的条目没有资源,进拍摄页会黑屏卡死 + if (viewStart > nowSec) { ++skippedFuture; continue; } + + if (!isHead) { + if (seen.emplace(id->ToString()).second) ret.emplace_back(id->ToString()); + continue; + } + for (auto headGetter : { getHeadId, getDefaultHeadId }) { + auto headId = headGetter->Invoke(item); + if (!headId) continue; + auto headIdStr = headId->ToString(); + if (!headIdStr.empty() && seen.emplace(headIdStr).second) ret.emplace_back(headIdStr); + } + } + if (skippedFuture) Log::InfoFmt("GetMasterCostumeIdsAll(isHead=%d): skipped future entries=%d", isHead, skippedFuture); + return ret; + } + + // 把缺失的主表服装/头部克隆成用户记录,直接 Add 进集合本体; + // 这样 GetAll()(字典 Values 视图,拍摄页 CreateItemModels 用它)等所有读取路径都能看到 + bool AddMissingCostumesToCollection(void* self, const std::string& klassName) { + UnityResolve::Method *Clone, *getId, *setId; + GetIdolIdType idType; + if (klassName == "UserCostumeHeadCollection") { + static auto head_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "Clone"); + static auto head_getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "get_CostumeHeadId"); + static auto head_setId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostumeHead", "set_CostumeHeadId"); + Clone = head_Clone; getId = head_getId; setId = head_setId; + idType = GetIdolIdType::CostumeHeadId; + } + else if (klassName == "UserCostumeCollection") { + static auto body_Clone = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "Clone"); + static auto body_getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "get_CostumeId"); + static auto body_setId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Transaction", "UserCostume", "set_CostumeId"); + Clone = body_Clone; getId = body_getId; setId = body_setId; + idType = GetIdolIdType::CostumeId; + } + else return false; + if (!Clone || !getId || !setId) return false; + + auto collectionKlass = Il2cppUtils::get_class_from_instance(self); + if (!collectionKlass || klassName != collectionKlass->name) return false; + // 共享泛型方法必须使用实际运行时集合类型的 MethodInfo,并传入末尾隐藏参数。 + auto getAllListMtd = Il2cppUtils::il2cpp_class_get_method_from_name(collectionKlass, "GetAllList", 1); + auto addMtd = Il2cppUtils::il2cpp_class_get_method_from_name(collectionKlass, "Add", 1); + if (!getAllListMtd || !addMtd) return false; + auto getAllList = reinterpret_cast(getAllListMtd->methodPointer); + auto collectionAdd = reinterpret_cast(addMtd->methodPointer); + // Exists 用于 Add 前复核:字典里可能存在 GetAllList 看不到的 key, + // 重复 Add 会在 native 栈上抛托管异常,直接崩 + auto existsMtd = Il2cppUtils::il2cpp_class_get_method_from_name(collectionKlass, "Exists", 1); + auto collectionExists = existsMtd + ? reinterpret_cast(existsMtd->methodPointer) + : nullptr; + + auto list = getAllList(self, nullptr, (void*)getAllListMtd); + Il2cppUtils::Tools::CSListEditor listEditor(list); + if (listEditor.get_Count() <= 0) return false; // 无模板可克隆,数据未加载完,等下次调用 + + std::unordered_set existIds{}; + for (auto i : listEditor) { + auto id = getId->Invoke(i); + if (id) existIds.emplace(id->ToString()); + } + + auto allIds = GetIdolMusicIdAll("", idType); + // 颜色组锚点始终用 body 服装 ID(head 的异色也是通过 body 行的颜色组关联的) + auto bodyIds = GetIdolMusicIdAll("", GetIdolIdType::CostumeId); + std::unordered_set baseIds(bodyIds.begin(), bodyIds.end()); + auto masterIds = GetMasterCostumeIdsAll(idType == GetIdolIdType::CostumeHeadId, baseIds); + allIds.insert(allIds.end(), masterIds.begin(), masterIds.end()); + int added = 0; + for (auto& id : allIds) { + // emplace 兼做去重:两个来源有重叠,重复 Add 同一 key 会抛异常 + if (id.empty() || !existIds.emplace(id).second) continue; + auto idStr = Il2cppString::New(id); + if (collectionExists && collectionExists(self, idStr, (void*)existsMtd)) continue; + auto clone = Clone->Invoke(listEditor.get_Item(0)); + setId->Invoke(clone, idStr); + collectionAdd(self, clone, (void*)addMtd); + ++added; + } + Log::InfoFmt("Costume collection augment: %s, added=%d", klassName.c_str(), added); + return true; + } + + // 集合当前条目数,取不到返回 -1 + int GetUserDataCollectionCount(void* self, void* klass) { + auto mtd = Il2cppUtils::il2cpp_class_get_method_from_name(klass, "get_Count", 0); + if (!mtd) return -1; + return reinterpret_cast(mtd->methodPointer)(self, (void*)mtd); + } + + DEFINE_HOOK(void*, UserDataCollection_GetAll, (void* self, void* mtd)) { + if (Config::dbgMode && Config::unlockAllLiveCostume) { + auto this_klass = Il2cppUtils::get_class_from_instance(self); + const char* klassName = this_klass ? this_klass->name : nullptr; + // GetAll 是父类共享泛型实现,游戏内所有 UserDataCollection 都会命中这里, + // 属于热路径,先用 strcmp 早退,别构造 std::string + if (klassName && (strcmp(klassName, "UserCostumeCollection") == 0 + || strcmp(klassName, "UserCostumeHeadCollection") == 0)) { + // 按实例指针记住"已补过",再用条目数复核:Boehm GC 不移动对象,但会复用 + // 已释放的地址,新集合落到旧地址上时条目数对不上,于是重新补一次。 + // 取不到 Count(返回 -1)时退化成"每个指针只补一次"的旧行为。 + static std::unordered_map augmentedCounts{}; + const auto it = augmentedCounts.find(self); + if (it == augmentedCounts.end() || it->second != GetUserDataCollectionCount(self, this_klass)) { + if (AddMissingCostumesToCollection(self, klassName)) { + augmentedCounts[self] = GetUserDataCollectionCount(self, this_klass); + } + } + } + } + return UserDataCollection_GetAll_Orig(self, mtd); + } + + DEFINE_HOOK(bool, PhotographyCostumeSettingListItemModel_get_IsDisabled, (void* self, void* mtd)) { + return Config::dbgMode && Config::unlockAllLiveCostume + ? false + : PhotographyCostumeSettingListItemModel_get_IsDisabled_Orig(self, mtd); + } + + void* AddPhotographyIdolSkinItems(void* list, const std::string& characterId, + UnityResolve::Method* getItem, UnityResolve::Method* getId) { + if (!list || characterId.empty() || !getItem || !getId) return list; + + static auto get_IdolCardSkinMaster = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "MasterManager", "get_IdolCardSkinMaster"); + static auto Master_GetAllWithSortByKey = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Master", "IdolCardSkinMaster", "GetAllWithSortByKey"); + static auto IdolCardSkin_get_IdolCardId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "get_IdolCardId"); + if (!get_IdolCardSkinMaster || !Master_GetAllWithSortByKey || !IdolCardSkin_get_IdolCardId) return list; + + Il2cppUtils::Tools::CSListEditor listEditor(list); + std::unordered_set ids; + for (auto item : listEditor) { + auto id = getId->Invoke(item); + if (id) ids.emplace(id->ToString()); + } + + auto idolCardSkinMaster = get_IdolCardSkinMaster->Invoke(nullptr); + auto idolCardSkinList = idolCardSkinMaster + ? Master_GetAllWithSortByKey->Invoke*>(idolCardSkinMaster, 0, nullptr) + : nullptr; + if (!idolCardSkinList) return list; + + int added = 0; + const auto cardPrefix = "i_card-" + characterId; + for (auto idolCardSkin : idolCardSkinList->ToArray()->ToVector()) { + if (!idolCardSkin) continue; // ToVector 会包含 List 容量内的 null 槽位,与 GetIdolMusicIdAll 一致 + auto idolCardId = IdolCardSkin_get_IdolCardId->Invoke(idolCardSkin); + if (!idolCardId || !idolCardId->ToString().starts_with(cardPrefix)) continue; + + auto item = getItem->Invoke(idolCardSkin); + auto id = item ? getId->Invoke(item) : nullptr; + if (id && ids.emplace(id->ToString()).second) { + listEditor.Add(item); + ++added; + } + } + Log::InfoFmt("Photography add: character=%s, added=%d", characterId.c_str(), added); + return list; + } + + DEFINE_HOOK(void*, PhotographyCostumeSettingData_GetCostumes, + (void* self, Il2cppString* characterId, void* mtd)) { + auto ret = PhotographyCostumeSettingData_GetCostumes_Orig(self, characterId, mtd); + if (!(Config::dbgMode && Config::unlockAllLiveCostume)) return ret; + + static auto getItem = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "GetCostume"); + static auto getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "Costume", "get_Id"); + return AddPhotographyIdolSkinItems(ret, characterId ? characterId->ToString() : "", getItem, getId); + } + + DEFINE_HOOK(void*, PhotographyCostumeSettingData_GetCostumeHeads, + (void* self, Il2cppString* characterId, void* mtd)) { + auto ret = PhotographyCostumeSettingData_GetCostumeHeads_Orig(self, characterId, mtd); + if (!(Config::dbgMode && Config::unlockAllLiveCostume)) return ret; + + static auto getItem = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "IdolCardSkin", "GetCostumeHead"); + static auto getId = Il2cppUtils::GetMethod("Assembly-CSharp.dll", "Campus.Common.Proto.Client.Master", "CostumeHead", "get_Id"); + return AddPhotographyIdolSkinItems(ret, characterId ? characterId->ToString() : "", getItem, getId); + } + void* getCompletedUniTask() { static auto unitask_klass = Il2cppUtils::GetClass("UniTask.dll", "Cysharp.Threading.Tasks", "UniTask"); static auto CompletedTask_field = unitask_klass->Get("CompletedTask"); @@ -1270,16 +1806,23 @@ namespace GakumasLocal::HookMain { return PictureBookLiveSelectScreenPresenter_MoveLiveScene_Orig(self, produceLive, isPlayCharacterFocusCamera, mtd); } + // 进入 Live 时歌词默认显示:仅对每个 Presenter 实例的第一次设置强制 true, + // 之后(玩家的手动开关)原样透传,不再覆盖关闭操作。 + std::unordered_set g_lyricsInitialForced{}; + DEFINE_HOOK(void, LiveSceneModel_set_IsLyricsActive, (void* self, bool value, void* mtd)) { - return LiveSceneModel_set_IsLyricsActive_Orig(self, Config::dbgMode && Config::unlockAllLive ? true : value, mtd); + return LiveSceneModel_set_IsLyricsActive_Orig(self, value, mtd); } DEFINE_HOOK(void, LiveScenePresenter_SetLyricsActive, (void* self, bool value, void* mtd)) { - return LiveScenePresenter_SetLyricsActive_Orig(self, Config::dbgMode && Config::unlockAllLive ? true : value, mtd); + if (Config::dbgMode && Config::unlockAllLive && self && g_lyricsInitialForced.insert(self).second) { + value = true; + } + return LiveScenePresenter_SetLyricsActive_Orig(self, value, mtd); } DEFINE_HOOK(void, LiveSceneContentView_SetLyricsTextActive, (void* self, bool value, void* mtd)) { - return LiveSceneContentView_SetLyricsTextActive_Orig(self, Config::dbgMode && Config::unlockAllLive ? true : value, mtd); + return LiveSceneContentView_SetLyricsTextActive_Orig(self, value, mtd); } // std::string lastMusicId; @@ -1931,6 +2474,12 @@ namespace GakumasLocal::HookMain { ADD_HOOK(MessageExtensions_MergeFrom, Il2cppUtils::GetMethodPointer("Google.Protobuf.dll", "Google.Protobuf", "MessageExtensions", "MergeFrom", {"Google.Protobuf.IMessage", "System.ReadOnlySpan"})); + ADD_HOOK(ExamExtensions_ExchangeSpToNormal, + Il2cppUtils::GetMethodPointer( + "Assembly-CSharp.dll", "Campus.InGame", "ExamExtensions", + "ExchangeSpToNormal", + { "Campus.Common.Proto.Client.Enums.ProduceStepType" })); + /* // 此 block 为 MasterBase 相关的 hook,后来发现它们最后都会调用 MessageExtensions.MergeFrom 进行构造,遂停用。现留档以备用 // ADD_HOOK(MasterBase_GetAll, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master", // "MasterBase`2", "GetAll", {"*", "*", "*", "*", "*"})); @@ -1993,6 +2542,22 @@ namespace GakumasLocal::HookMain { ADD_HOOK(UserCostumeCollection_FindBy, UserCostumeCollection_FindBy_mtd->methodPointer); } + auto UserCostumeCollection_GetAll_mtd = Il2cppUtils::il2cpp_class_get_method_from_name( + UserCostumeCollection_klass->address, "GetAll", 0); + if (UserCostumeCollection_GetAll_mtd) { + ADD_HOOK(UserDataCollection_GetAll, UserCostumeCollection_GetAll_mtd->methodPointer); + } + + ADD_HOOK(PhotographyCostumeSettingListItemModel_get_IsDisabled, + Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Photography", + "PhotographyCostumeSettingListItemModel", "get_IsDisabled")); + ADD_HOOK(PhotographyCostumeSettingData_GetCostumes, + Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Photography", + "PhotographyCostumeSettingData", "GetCostumes")); + ADD_HOOK(PhotographyCostumeSettingData_GetCostumeHeads, + Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Photography", + "PhotographyCostumeSettingData", "GetCostumeHeads")); + // 双端 ADD_HOOK(PictureBookLiveThumbnailView_SetReleaseDataAsync, GetMethodPointerByArgCount("Assembly-CSharp.dll", "Campus.OutGame.PictureBook", @@ -2142,6 +2707,17 @@ namespace GakumasLocal::HookMain { "UnityEngine.Transform::set_position_Injected(UnityEngine.Vector3&)")); ADD_HOOK(Unity_set_rotation_Injected, Il2cppUtils::il2cpp_resolve_icall( "UnityEngine.Transform::set_rotation_Injected(UnityEngine.Quaternion&)")); + ADD_HOOK(Unity_SetPositionAndRotation_Injected, Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.Transform::SetPositionAndRotation_Injected(UnityEngine.Vector3&,UnityEngine.Quaternion&)")); + ADD_HOOK(Unity_set_localPosition_Injected, Il2cppUtils::il2cpp_resolve_icall( + "UnityEngine.Transform::set_localPosition_Injected(UnityEngine.Vector3&)")); + ADD_HOOK(Camera_get_main, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", + "Camera", "get_main")); + ADD_HOOK(CinemachineBrain_PushStateToUnityCamera, + Il2cppUtils::GetMethodPointer("Cinemachine.dll", "Cinemachine", + "CinemachineBrain", "PushStateToUnityCamera")); + ADD_HOOK(Component_get_transform, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", + "Component", "get_transform")); ADD_HOOK(Unity_get_fieldOfView, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Camera", "get_fieldOfView")); ADD_HOOK(Unity_set_fieldOfView, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", diff --git a/app/src/main/cpp/GakumasLocalify/camera/camera.cpp b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp index ebd8ba7..23e5d26 100644 --- a/app/src/main/cpp/GakumasLocalify/camera/camera.cpp +++ b/app/src/main/cpp/GakumasLocalify/camera/camera.cpp @@ -1,6 +1,7 @@ #include "baseCamera.hpp" #include "camera.hpp" #include +#include #include "../Misc.hpp" #include "../BaseDefine.h" #include "../../platformDefine.hpp" @@ -411,6 +412,14 @@ namespace GKCamera { const auto currentY = targetPos.y; static auto lastRetY = currentY; + // NaN/Inf 防护:目标 Y 非法时保持上一次有效值,避免污染平滑队列后相机飞走 + if (!std::isfinite(currentY)) { + return std::isfinite(lastRetY) ? lastRetY : 0.0f; + } + if (!std::isfinite(lastRetY)) { + lastRetY = currentY; + } + if (followModeY == FollowModeY::APPLY_Y) { lastRetY = currentY; return currentY; @@ -423,6 +432,10 @@ namespace GKCamera { recordsY.Push(currentY); } + if (!std::isfinite(currentAvg)) { + return lastRetY; + } + if (abs(currentY - currentAvg) < 0.02) { return lastRetY; } @@ -460,7 +473,17 @@ namespace GKCamera { // 计算角色的右方向 Vector3 up(0, 1, 0); // Y轴方向 - Vector3 right = forward.cross(up).Normalize(); + Vector3 right = forward.cross(up); + + // 修正归一化:原 UnityResolve::Vector3::Normalize 除的是平方长度,且零向量会产出 NaN + const auto rightLenSq = right.x * right.x + right.y * right.y + right.z * right.z; + if (rightLenSq < 1e-8f) { + // forward 与 up 平行/接近或为零向量,无法求右向,退化为仅高度偏移 + return Vector3(position.x, position.y + offset.y, position.z); + } + const auto invRightLen = 1.0f / std::sqrt(rightLenSq); + right = Vector3(right.x * invRightLen, right.y * invRightLen, right.z * invRightLen); + Vector3 fwd = forward; Vector3 pos = position;