From bf67a9bedd074ad73a3ae52207f2dd4178aa5e84 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 5 Oct 2026 23:23:05 -0500 Subject: [PATCH 1/2] Extend Haven zoom with persistent road depth correction --- example_community_patch_settings_en.toml | 12 +- mods/src/config.cc | 8 + mods/src/config.h | 2 + mods/src/defaultconfig.h | 2 + mods/src/patches/parts/haven_road_depth.cc | 148 ++++++++++++ mods/src/patches/parts/haven_zoom.cc | 247 +++++++++++++++++++++ mods/src/patches/patches.cc | 2 + 7 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 mods/src/patches/parts/haven_road_depth.cc create mode 100644 mods/src/patches/parts/haven_zoom.cc diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 8c3a6f678..6f6ac12c1 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -240,9 +240,16 @@ system_zoom_preset_4 = 2750.0 # Zoom preset F5 for system view system_zoom_preset_5 = 5000.0 -# This controls the maximum zoom distance in System View. Default is 2500. Higher values increase label distance +# Maximum zoom-out distance in System View. Mod default is 5000. Higher values increase label distance. zoom = 5000.0 +# Maximum zoom-out distance in The Haven, in camera distance units (like zoom above). +# Higher values extend the native range; 0.0 keeps the native range. Default is 4000.0. +# Uses the normal scroll/pinch controls; this sets the range, not the current zoom. +# Expanded Haven zoom also keeps road surfaces separated from the ground at every zoom level. +# Requires [patches].havenzoomhooks; independent of the System View zoomhooks switch. +haven_zoom = 4000.0 + # <[=====================================================]> # # **** * * @@ -296,6 +303,9 @@ transitionscreenhooks = true uiscalehooks = true zoomhooks = true +# Compatibility switch for Haven zoom hooks. Use graphics.haven_zoom = 0.0 for native behavior. +havenzoomhooks = true + # <[==================================================================================================================================]> # # * * * * **** * * * diff --git a/mods/src/config.cc b/mods/src/config.cc index 27682b089..77194ddc0 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -942,6 +942,8 @@ void Config::Load() this->installUiScaleHooks = get_config_or_default(config, parsed, "patches", "uiscalehooks", DCP::uiscalehooks, write_config); this->installZoomHooks = get_config_or_default(config, parsed, "patches", "zoomhooks", DCP::zoomhooks, write_config); + this->installHavenZoomHooks = + get_config_or_default(config, parsed, "patches", "havenzoomhooks", DCP::havenzoomhooks, write_config); this->installBuffFixHooks = get_config_or_default(config, parsed, "patches", "bufffixhooks", DCP::bufffixhooks, write_config); this->installToastBannerHooks = @@ -1019,6 +1021,12 @@ void Config::Load() this->ui_scale_viewer = get_config_or_default(config, parsed, "graphics", "ui_scale_viewer", DCG::ui_scale_viewer, write_config); this->zoom = get_config_or_default(config, parsed, "graphics", "zoom", DCG::zoom, write_config); + this->haven_zoom = get_config_or_default(config, parsed, "graphics", "haven_zoom", DCG::haven_zoom, write_config); + if (!std::isfinite(this->haven_zoom) || this->haven_zoom < 0.0f) { + spdlog::warn("Invalid haven_zoom {}; using {}", this->haven_zoom, DCG::haven_zoom); + this->haven_zoom = DCG::haven_zoom; + parsed["graphics"].as_table()->insert_or_assign("haven_zoom", this->haven_zoom); + } this->fr_scale = get_config_or_default(config, parsed, "graphics", "fr_scale", DCG::fr_scale, write_config); this->zoom_label_player.detail = get_fleet_label_detail(config, parsed, "zoom_label_player_detail", DCG::zoom_label_player_detail, write_config); diff --git a/mods/src/config.h b/mods/src/config.h index c00634090..c2988b585 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -173,6 +173,7 @@ class Config final float ui_scale_ship; float ui_scale_viewer; float zoom; + float haven_zoom; float fr_scale; FleetLabelProfile zoom_label_player; FleetLabelProfile zoom_label_non_player; @@ -269,6 +270,7 @@ class Config final bool installUiScaleHooks; bool installZoomHooks; + bool installHavenZoomHooks; bool installBuffFixHooks; bool installToastBannerHooks; bool installPanHooks; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 8609e8333..563d752cf 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -60,6 +60,7 @@ namespace Graphics constexpr auto ui_scale_viewer = 1.2; constexpr bool use_presets_as_default = true; constexpr auto zoom = 5000; + constexpr auto haven_zoom = 4000.0; constexpr auto fr_scale = 2.0; constexpr bool loader_enabled = true; // replace LoginSequence background constexpr bool loader_transition = true; // replace TVC/SlideShow backgrounds @@ -90,6 +91,7 @@ namespace Patches constexpr bool toastbannerhooks = true; constexpr bool uiscalehooks = true; constexpr bool zoomhooks = true; + constexpr bool havenzoomhooks = true; constexpr bool miscpatches = true; constexpr bool giftsbulkclaimhooks = true; constexpr bool dailyfactionbulkclaimhooks = true; diff --git a/mods/src/patches/parts/haven_road_depth.cc b/mods/src/patches/parts/haven_road_depth.cc new file mode 100644 index 000000000..5e326c7a3 --- /dev/null +++ b/mods/src/patches/parts/haven_road_depth.cc @@ -0,0 +1,148 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ +struct RoadDepthApi { + Il2CppClass *renderer = nullptr; + const MethodInfo *find = nullptr, *children = nullptr, *materials = nullptr; + const MethodInfo *name = nullptr, *shader = nullptr, *has = nullptr, *get = nullptr, *set = nullptr; + + RoadDepthApi() + { + auto cls = [](const char *n) { + return il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine", n).get_cls(); + }; + auto *game_object = cls("GameObject"), *material = cls("Material"); + renderer = cls("Renderer"); + find = method_contract::Resolve(game_object, "Find", true, "UnityEngine.GameObject", {"System.String"}); + children = method_contract::Resolve(game_object, "GetComponentsInChildren", false, "UnityEngine.Component[]", + {"System.Type", "System.Boolean"}); + materials = method_contract::Resolve(renderer, "get_sharedMaterials", false, "UnityEngine.Material[]", {}); + name = method_contract::Resolve(cls("Object"), "get_name", false, "System.String", {}); + shader = method_contract::Resolve(material, "get_shader", false, "UnityEngine.Shader", {}); + has = method_contract::Resolve(material, "HasProperty", false, "System.Boolean", {"System.String"}); + get = method_contract::Resolve(material, "GetFloat", false, "System.Single", {"System.String"}); + set = method_contract::Resolve(material, "SetFloat", false, "System.Void", {"System.String", "System.Single"}); + } + + bool Valid() const + { return renderer && find && children && materials && name && shader && has && get && set; } +}; + +Il2CppObject *Invoke(const MethodInfo *method, Il2CppObject *object, void **args = nullptr) +{ + Il2CppObject *result = nullptr; + return Il2CppRuntime::TryInvoke(method, object, args, &result) ? result : nullptr; +} + +std::string Name(RoadDepthApi &api, Il2CppObject *object) +{ + auto *value = object ? reinterpret_cast(Invoke(api.name, object)) : nullptr; + return value && value->length <= 256 ? to_string(value) : std::string{}; +} + +bool IsRoad(const std::string &name) +{ + for (const char *prefix : + {"mat_PB_3101101_asphalt_", "mat_PB_3101201_asphalt_", "mat_PB_3101301_asphalt_", "mat_PB_3101401_asphalt_", + "mat_PB_3101501_asphalt_", "mat_PB_3101601_asphalt_", "mat_PB_3101701_asphalt_"}) { + if (name.starts_with(prefix)) + return true; + } + return false; +} + +bool ReadDepth(RoadDepthApi &api, Il2CppObject *material, Il2CppString *property, float &value) +{ + void *args[] = {property}; + bool present = false; + if (!Il2CppRuntime::TryBoolean(Invoke(api.has, material, args), present) || !present) + return false; + auto *boxed = Invoke(api.get, material, args); + if (!boxed || !method_contract::Type(il2cpp_class_get_type(boxed->klass), "System.Single")) + return false; + value = *static_cast(il2cpp_object_unbox(boxed)); + return std::isfinite(value); +} +} // namespace + +// Called only by the qualified, enabled Haven camera path. The small bias stays +// on at every zoom level, keeping nearly coplanar road surfaces above the ground. +// All scene objects are resolved afresh; no Unity pointers or GC handles are retained. +void ApplyHavenRoadDepthBias() +{ + using Clock = std::chrono::steady_clock; + static auto next = Clock::time_point{}; + if (Clock::now() < next) + return; + next = Clock::now() + std::chrono::seconds(1); + static RoadDepthApi api; + if (!api.Valid()) { + static bool warned = false; + if (!warned) { + spdlog::warn("[HavenZoom] road material API unavailable; keeping native road depth"); + warned = true; + } + return; + } + void *root_args[] = {il2cpp_string_new("StarbaseRoot/StarbaseManager/StarbasePlanetaryVisualsHolder")}; + auto *root = Invoke(api.find, nullptr, root_args); + if (!root) + return; + bool include_inactive = true; + void *child_args[] = {il2cpp_type_get_object(il2cpp_class_get_type(api.renderer)), &include_inactive}; + auto *renderers = reinterpret_cast(Invoke(api.children, root, child_args)); + if (!renderers || renderers->max_length > 4096) + return; + for (uintptr_t i = 0; i < renderers->max_length; ++i) { + auto *renderer = reinterpret_cast(renderers->vector[i]); + if (!renderer) + continue; + auto *materials = reinterpret_cast(Invoke(api.materials, renderer)); + if (!materials || materials->max_length > 16) + continue; + for (uintptr_t j = 0; j < materials->max_length; ++j) { + auto *material = reinterpret_cast(materials->vector[j]); + const auto name = Name(api, material); + if (!material || !IsRoad(name) || Name(api, Invoke(api.shader, material)) != "Custom/LitMSAE") + continue; + const std::array properties = {il2cpp_string_new("_OffsetFactor"), + il2cpp_string_new("_OffsetUnits")}; + std::array before{}; + if (!ReadDepth(api, material, properties[0], before[0]) || !ReadDepth(api, material, properties[1], before[1])) + continue; + bool changed = false, verified = true; + for (unsigned k = 0; k < 2; ++k) { + // Preserve an already stronger native bias. + float target = std::min(before[k], -1.0f); + if (target == before[k]) + continue; + void *args[] = {properties[k], &target}; + if (!Il2CppRuntime::TryInvoke(api.set, material, args)) { + verified = false; + continue; + } + changed = true; + float after = 0.0f; + verified = ReadDepth(api, material, properties[k], after) && after == target && verified; + } + if (changed) { + spdlog::debug("[HavenZoom] road depth bias {}: verified={}", name, verified); + static bool reported = false; + if (verified && !reported) { + spdlog::info("[HavenZoom] corrected road surface depth for expanded Haven zoom"); + reported = true; + } + } + } + } +} diff --git a/mods/src/patches/parts/haven_zoom.cc b/mods/src/patches/parts/haven_zoom.cc new file mode 100644 index 000000000..7f5f2b669 --- /dev/null +++ b/mods/src/patches/parts/haven_zoom.cc @@ -0,0 +1,247 @@ +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +void ApplyHavenRoadDepthBias(); + +namespace +{ +Il2CppClass *planetary_provider_class = nullptr; +FieldInfo *blend_source = nullptr, *blend_target = nullptr, *blend_curve = nullptr; +FieldInfo *blend_minimum = nullptr, *blend_maximum = nullptr, *blend_ratio = nullptr; +FieldInfo *blend_source_frame = nullptr, *blend_target_frame = nullptr, *blend_result_frame = nullptr; +FieldInfo *provider_constraint = nullptr, *provider_radius = nullptr, *constraint_radius = nullptr; +FieldInfo *provider_pivot = nullptr, *provider_look_target = nullptr; +FieldInfo *radius_enabled = nullptr, *radius_minimum = nullptr, *radius_maximum = nullptr; +FieldInfo *frame_position = nullptr, *frame_rotation = nullptr, *frame_fov = nullptr; +FieldInfo *frame_far_clip = nullptr, *frame_orthographic = nullptr; +const MethodInfo *curve_evaluate = nullptr; + +struct HavenVector { + float x, y, z; +}; +struct HavenRotation { + float x, y, z, w; +}; + +FieldInfo *HavenField(Il2CppClass *cls, const char *name, const char *type) +{ + auto *field = cls != nullptr ? il2cpp_class_get_field_from_name(cls, name) : nullptr; + return field != nullptr && !(il2cpp_field_get_flags(field) & FIELD_ATTRIBUTE_STATIC) + && method_contract::Type(field->type, type) + ? field + : nullptr; +} + +FieldInfo *HavenReferenceField(Il2CppClass *cls, const char *name, Il2CppClass *expected) +{ + auto *field = cls != nullptr ? il2cpp_class_get_field_from_name(cls, name) : nullptr; + return field != nullptr && field->type != nullptr && !field->type->byref && expected != nullptr + && !(il2cpp_field_get_flags(field) & FIELD_ATTRIBUTE_STATIC) && !il2cpp_class_is_valuetype(expected) + && il2cpp_class_from_type(field->type) == expected + ? field + : nullptr; +} + +template T ReadHavenField(Il2CppObject *object, FieldInfo *field) +{ + T value{}; + il2cpp_field_get_value(object, field, &value); + return value; +} + +bool ReadHavenRadius(Il2CppObject *provider, float &radius) +{ + if (provider == nullptr || !il2cpp_class_is_assignable_from(planetary_provider_class, provider->klass)) + return false; + auto *constraint = ReadHavenField(provider, provider_constraint); + auto *data = constraint != nullptr ? ReadHavenField(constraint, constraint_radius) : nullptr; + if (data == nullptr || !ReadHavenField(data, radius_enabled)) + return false; + const auto minimum = ReadHavenField(data, radius_minimum); + const auto maximum = ReadHavenField(data, radius_maximum); + radius = ReadHavenField(provider, provider_radius); + return std::isfinite(radius) && std::isfinite(maximum) && maximum > 0.0f && minimum == maximum + && std::abs(radius - maximum) < 0.01f; +} + +void HavenCamera_UpdateCameraFrame_Hook(auto original, Il2CppObject *provider, Camera *camera) +{ + // Native input, constraints and endpoint updates run first. Only this blend's + // newly generated output is extended; endpoint assets and normalized LOD stay native. + original(provider, camera); + const auto max_zoom = Config::Get().haven_zoom; + if (provider == nullptr || !std::isfinite(max_zoom) || max_zoom <= 0.0f) + return; + + auto *source = ReadHavenField(provider, blend_source); + auto *target = ReadHavenField(provider, blend_target); + float source_radius = 0.0f, target_radius = 0.0f; + if (!ReadHavenRadius(source, source_radius) || !ReadHavenRadius(target, target_radius) + || source_radius == target_radius || max_zoom <= std::max(source_radius, target_radius)) + return; + + auto *pivot = ReadHavenField(source, provider_pivot); + if (pivot == nullptr || ReadHavenField(target, provider_pivot) != pivot + || ReadHavenField(source, provider_look_target) != pivot + || ReadHavenField(target, provider_look_target) != pivot) + return; + auto *source_frame = ReadHavenField(provider, blend_source_frame); + auto *target_frame = ReadHavenField(provider, blend_target_frame); + auto *result = ReadHavenField(provider, blend_result_frame); + auto *curve = ReadHavenField(provider, blend_curve); + const auto minimum = ReadHavenField(provider, blend_minimum); + const auto maximum = ReadHavenField(provider, blend_maximum); + const auto ratio = ReadHavenField(provider, blend_ratio); + if (source_frame == nullptr || target_frame == nullptr || result == nullptr || curve == nullptr + || source_frame == target_frame || result == source_frame || result == target_frame || !std::isfinite(minimum) + || !std::isfinite(maximum) || !std::isfinite(ratio) || minimum < 0.0f || maximum > 1.0f || maximum <= minimum) + return; + + auto position = ReadHavenField(result, frame_position); + const auto ar = ReadHavenField(source_frame, frame_rotation); + const auto br = ReadHavenField(target_frame, frame_rotation); + const auto af = ReadHavenField(source_frame, frame_fov); + const auto bf = ReadHavenField(target_frame, frame_fov); + const auto rotation_dot = ar.x * br.x + ar.y * br.y + ar.z * br.z + ar.w * br.w; + const auto gap = std::abs(source_radius - target_radius); + const auto rotation_length = ar.x * ar.x + ar.y * ar.y + ar.z * ar.z + ar.w * ar.w; + // Qualify the common-pivot, same-facing perspective orbit measured in Haven. + // Preserve native output when a client changes that geometry or camera mode. + if (!std::isfinite(rotation_length) || std::abs(rotation_length - 1.0f) > 0.0001f || !std::isfinite(rotation_dot) + || std::abs(std::abs(rotation_dot) - 1.0f) > 0.0001f || !std::isfinite(af) || !std::isfinite(bf) || af <= 0.0f + || std::abs(af - bf) > 0.001f || ReadHavenField(source_frame, frame_orthographic) + || ReadHavenField(target_frame, frame_orthographic) || ReadHavenField(result, frame_orthographic)) + return; + + const auto evaluate = + reinterpret_cast(curve_evaluate->methodPointer); + const auto at_ratio = evaluate(curve, ratio, curve_evaluate); + const auto at_minimum = evaluate(curve, minimum, curve_evaluate); + const auto at_maximum = evaluate(curve, maximum, curve_evaluate); + if (!std::isfinite(at_ratio) || !std::isfinite(at_minimum) || !std::isfinite(at_maximum)) + return; + const bool source_is_far = source_radius > target_radius; + const auto outer_weight = + source_is_far ? 1.0f - std::clamp(at_minimum, 0.0f, 1.0f) : std::clamp(at_maximum, 0.0f, 1.0f); + const auto inner_weight = + source_is_far ? 1.0f - std::clamp(at_maximum, 0.0f, 1.0f) : std::clamp(at_minimum, 0.0f, 1.0f); + if (outer_weight <= inner_weight) + return; + const auto weight = source_is_far ? 1.0 - std::clamp(at_ratio, 0.0f, 1.0f) : std::clamp(at_ratio, 0.0f, 1.0f); + const auto inner_radius = std::min(source_radius, target_radius); + const auto native_outer = inner_radius + gap * outer_weight; + const auto native_distance = inner_radius + gap * weight; + const auto progress = std::clamp((weight - inner_weight) / (outer_weight - inner_weight), 0.0, 1.0); + const auto extra = std::min(max_zoom - native_distance, (max_zoom - native_outer) * progress); + if (!std::isfinite(extra)) + return; + ApplyHavenRoadDepthBias(); + if (extra <= 0.0) + return; + + // Sequential endpoint updates can cache different positions of their shared + // pivot during pan. Use the camera back axis so expansion remains steady. + position.x += static_cast(-2.0 * (ar.x * ar.z + ar.w * ar.y) * extra); + position.y += static_cast(-2.0 * (ar.y * ar.z - ar.w * ar.x) * extra); + position.z += static_cast((2.0 * (ar.x * ar.x + ar.y * ar.y) - 1.0) * extra); + auto clip = ReadHavenField(result, frame_far_clip); + if (!std::isfinite(clip) || clip <= 0.0f) + return; + clip += static_cast(extra); + if (!std::isfinite(position.x) || !std::isfinite(position.y) || !std::isfinite(position.z) || !std::isfinite(clip) + || clip <= 0.0f) + return; + il2cpp_field_set_value(result, frame_position, &position); + il2cpp_field_set_value(result, frame_far_clip, &clip); + static bool reported = false; + if (!reported) { + spdlog::info("[HavenZoom] expanded native outer distance {} to maximum {}", native_outer, max_zoom); + reported = true; + } +} +} // namespace + +void InstallHavenZoomHooks() +{ + auto blend = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "BlendFrameProvider"); + auto frame_provider = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "FrameProvider"); + auto orbit = + il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "AbstractOrbitFrameProvider"); + auto target = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "AbstractTargetController"); + auto planetary = + il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "PlanetaryOrbitFrameProvider"); + auto orbit_constraint = + il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "OrbitConstraint"); + auto constraint = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "Constraint"); + auto frame = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "CameraFrame"); + auto editable = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.CameraController", "EditableCameraFrame"); + auto curve = il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine", "AnimationCurve"); + Il2CppClass *radius_class = nullptr; + if (constraint.get_cls() != nullptr) { + void *iterator = nullptr; + while (auto *nested = il2cpp_class_get_nested_types(constraint.get_cls(), &iterator)) { + if (std::strcmp(il2cpp_class_get_name(nested), "FloatData") == 0) { + radius_class = nested; + break; + } + } + } + const auto *update = + method_contract::Resolve(blend.get_cls(), "UpdateCameraFrame", false, "System.Void", {"UnityEngine.Camera"}); + curve_evaluate = method_contract::Resolve(curve.get_cls(), "Evaluate", false, "System.Single", {"System.Single"}); + planetary_provider_class = planetary.get_cls(); + blend_source = HavenReferenceField(blend.get_cls(), "_sourceFrameProvider", frame_provider.get_cls()); + blend_target = HavenReferenceField(blend.get_cls(), "_targetFrameProvider", frame_provider.get_cls()); + blend_curve = HavenReferenceField(blend.get_cls(), "_blendCurve", curve.get_cls()); + blend_minimum = HavenField(blend.get_cls(), "_softLimitMin", "System.Single"); + blend_maximum = HavenField(blend.get_cls(), "_softLimitMax", "System.Single"); + blend_ratio = HavenField(blend.get_cls(), "_blendRatio", "System.Single"); + blend_source_frame = HavenReferenceField(blend.get_cls(), "_sourceFrame", frame.get_cls()); + blend_target_frame = HavenReferenceField(blend.get_cls(), "_targetFrame", frame.get_cls()); + blend_result_frame = HavenReferenceField(blend.get_cls(), "_blendedCameraFrame", editable.get_cls()); + provider_constraint = HavenReferenceField(orbit.get_cls(), "_constraint", orbit_constraint.get_cls()); + provider_radius = HavenField(orbit.get_cls(), "_radius", "System.Single"); + provider_pivot = HavenReferenceField(orbit.get_cls(), "_orbitPivot", target.get_cls()); + provider_look_target = HavenReferenceField(orbit.get_cls(), "_lookTarget", target.get_cls()); + constraint_radius = HavenReferenceField(orbit_constraint.get_cls(), "_radius", radius_class); + radius_enabled = HavenField(radius_class, "_enabled", "System.Boolean"); + radius_minimum = HavenField(radius_class, "_min", "System.Single"); + radius_maximum = HavenField(radius_class, "_max", "System.Single"); + frame_position = HavenField(frame.get_cls(), "_position", "UnityEngine.Vector3"); + frame_rotation = HavenField(frame.get_cls(), "_rotation", "UnityEngine.Quaternion"); + frame_fov = HavenField(frame.get_cls(), "_FOV", "System.Single"); + frame_far_clip = HavenField(frame.get_cls(), "_farPlane", "System.Single"); + frame_orthographic = HavenField(frame.get_cls(), "_orthographic", "System.Boolean"); + if (update == nullptr || update->has_full_generic_sharing_signature || curve_evaluate == nullptr + || curve_evaluate->has_full_generic_sharing_signature || planetary_provider_class == nullptr + || orbit.get_cls() == nullptr || !il2cpp_class_is_assignable_from(orbit.get_cls(), planetary_provider_class)) { + spdlog::warn("[HavenZoom] camera API unavailable; keeping native zoom range"); + return; + } + for (auto *field : + {blend_source, blend_target, blend_curve, blend_minimum, blend_maximum, + blend_ratio, provider_pivot, provider_look_target, blend_source_frame, blend_target_frame, + blend_result_frame, provider_constraint, provider_radius, constraint_radius, radius_enabled, + radius_minimum, radius_maximum, frame_position, frame_rotation, frame_fov, + frame_far_clip, frame_orthographic}) { + if (field == nullptr) { + spdlog::warn("[HavenZoom] camera fields unavailable; keeping native zoom range"); + return; + } + } + // [patches].havenzoomhooks controls installation independently of haven_zoom. + if (SPUD_STATIC_DETOUR(update->methodPointer, HavenCamera_UpdateCameraFrame_Hook)) { + spdlog::info("[HavenZoom] installed planetary blend camera hook (maximum distance {})", Config::Get().haven_zoom); + } else { + spdlog::warn("[HavenZoom] camera hook was not installed; keeping native zoom range"); + } +} diff --git a/mods/src/patches/patches.cc b/mods/src/patches/patches.cc index ef00d4e16..5242a985f 100644 --- a/mods/src/patches/patches.cc +++ b/mods/src/patches/patches.cc @@ -20,6 +20,7 @@ void InstallUiScaleHooks(); void InstallZoomHooks(); +void InstallHavenZoomHooks(); void InstallBuffFixHooks(); #if _WIN32 void InstallFreeResizeHooks(); @@ -125,6 +126,7 @@ __int64 il2cpp_init_hook(auto original, const char* domain_name) const PatchEntry patches[] = { {"UiScaleHooks", {InstallUiScaleHooks, &cfg.installUiScaleHooks}}, {"ZoomHooks", {InstallZoomHooks, &cfg.installZoomHooks}}, + {"HavenZoomHooks", {InstallHavenZoomHooks, &cfg.installHavenZoomHooks}}, {"BuffFixHooks", {InstallBuffFixHooks, &cfg.installBuffFixHooks}}, {"ToastBannerHooks", {InstallToastBannerHooks, &cfg.installToastBannerHooks}}, {"PanHooks", {InstallPanHooks, &cfg.installPanHooks}}, From 7b09847a9176d1f737612c00888e7a4824ecdf17 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 5 Oct 2026 23:33:02 -0500 Subject: [PATCH 2/2] Reuse existing Vector3 for Haven camera output --- mods/src/patches/parts/haven_zoom.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mods/src/patches/parts/haven_zoom.cc b/mods/src/patches/parts/haven_zoom.cc index 7f5f2b669..f2945d421 100644 --- a/mods/src/patches/parts/haven_zoom.cc +++ b/mods/src/patches/parts/haven_zoom.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -25,9 +26,6 @@ FieldInfo *frame_position = nullptr, *frame_rotation = nullptr, *frame_fo FieldInfo *frame_far_clip = nullptr, *frame_orthographic = nullptr; const MethodInfo *curve_evaluate = nullptr; -struct HavenVector { - float x, y, z; -}; struct HavenRotation { float x, y, z, w; }; @@ -106,7 +104,7 @@ void HavenCamera_UpdateCameraFrame_Hook(auto original, Il2CppObject *provider, C || !std::isfinite(maximum) || !std::isfinite(ratio) || minimum < 0.0f || maximum > 1.0f || maximum <= minimum) return; - auto position = ReadHavenField(result, frame_position); + auto position = ReadHavenField(result, frame_position); const auto ar = ReadHavenField(source_frame, frame_rotation); const auto br = ReadHavenField(target_frame, frame_rotation); const auto af = ReadHavenField(source_frame, frame_fov);