diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index ec9b3ad8..3ab4e2c6 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 + # <[=====================================================]> # # **** * * @@ -297,6 +304,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 da3198c5..bc245d2e 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -952,6 +952,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 = @@ -1031,6 +1033,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 66788c1b..fa36dcd9 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; @@ -271,6 +272,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 40b31ae5..c06b807d 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 00000000..3c573fab --- /dev/null +++ b/mods/src/patches/parts/haven_road_depth.cc @@ -0,0 +1,163 @@ +#include +#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) { + if (!api.renderer) + spdlog::warn("[HavenZoom] road depth unavailable: missing UnityEngine.Renderer class"); + for (const auto& [method, signature] : + std::array, 8>{{ + {api.find, "GameObject.Find(String) -> GameObject [static]"}, + {api.children, "GameObject.GetComponentsInChildren(Type, Boolean) -> Component[]"}, + {api.materials, "Renderer.get_sharedMaterials() -> Material[]"}, + {api.name, "Object.get_name() -> String"}, + {api.shader, "Material.get_shader() -> Shader"}, + {api.has, "Material.HasProperty(String) -> Boolean"}, + {api.get, "Material.GetFloat(String) -> Single"}, + {api.set, "Material.SetFloat(String, Single) -> Void"}}}) { + if (!method) + spdlog::warn("[HavenZoom] keeping native road depth: missing or incompatible UnityEngine.{}", signature); + } + 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 00000000..f267e484 --- /dev/null +++ b/mods/src/patches/parts/haven_zoom.cc @@ -0,0 +1,296 @@ +#include "config.h" + +#include +#include +#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 HavenRotation { + float x, y, z, w; +}; + +bool ReadHavenRadius(Il2CppObject *provider, float &radius) +{ + if (provider == nullptr || !il2cpp_class_is_assignable_from(planetary_provider_class, provider->klass)) + return false; + Il2CppObject *constraint = nullptr, *data = nullptr; + il2cpp_field_get_value(provider, provider_constraint, &constraint); + if (constraint == nullptr) + return false; + il2cpp_field_get_value(constraint, constraint_radius, &data); + if (data == nullptr) + return false; + bool enabled = false; + il2cpp_field_get_value(data, radius_enabled, &enabled); + if (!enabled) + return false; + float minimum = 0.0f, maximum = 0.0f; + il2cpp_field_get_value(data, radius_minimum, &minimum); + il2cpp_field_get_value(data, radius_maximum, &maximum); + il2cpp_field_get_value(provider, provider_radius, &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; + + Il2CppObject *source = nullptr, *target = nullptr; + il2cpp_field_get_value(provider, blend_source, &source); + il2cpp_field_get_value(provider, blend_target, &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; + + Il2CppObject *pivot = nullptr, *target_pivot = nullptr, *source_look_target = nullptr, *target_look_target = nullptr; + il2cpp_field_get_value(source, provider_pivot, &pivot); + il2cpp_field_get_value(target, provider_pivot, &target_pivot); + il2cpp_field_get_value(source, provider_look_target, &source_look_target); + il2cpp_field_get_value(target, provider_look_target, &target_look_target); + if (pivot == nullptr || target_pivot != pivot || source_look_target != pivot || target_look_target != pivot) + return; + Il2CppObject *source_frame = nullptr, *target_frame = nullptr, *result = nullptr, *curve = nullptr; + float minimum = 0.0f, maximum = 0.0f, ratio = 0.0f; + il2cpp_field_get_value(provider, blend_source_frame, &source_frame); + il2cpp_field_get_value(provider, blend_target_frame, &target_frame); + il2cpp_field_get_value(provider, blend_result_frame, &result); + il2cpp_field_get_value(provider, blend_curve, &curve); + il2cpp_field_get_value(provider, blend_minimum, &minimum); + il2cpp_field_get_value(provider, blend_maximum, &maximum); + il2cpp_field_get_value(provider, blend_ratio, &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; + + Vector3 position{}; + HavenRotation ar{}, br{}; + float af = 0.0f, bf = 0.0f; + bool source_orthographic = false, target_orthographic = false, result_orthographic = false; + il2cpp_field_get_value(result, frame_position, &position); + il2cpp_field_get_value(source_frame, frame_rotation, &ar); + il2cpp_field_get_value(target_frame, frame_rotation, &br); + il2cpp_field_get_value(source_frame, frame_fov, &af); + il2cpp_field_get_value(target_frame, frame_fov, &bf); + il2cpp_field_get_value(source_frame, frame_orthographic, &source_orthographic); + il2cpp_field_get_value(target_frame, frame_orthographic, &target_orthographic); + il2cpp_field_get_value(result, frame_orthographic, &result_orthographic); + 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 || source_orthographic || target_orthographic || result_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); + float clip = 0.0f; + il2cpp_field_get_value(result, frame_far_clip, &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"); + for (const auto &[name, helper] : {std::pair{"BlendFrameProvider", &blend}, + {"FrameProvider", &frame_provider}, + {"AbstractOrbitFrameProvider", &orbit}, + {"AbstractTargetController", &target}, + {"PlanetaryOrbitFrameProvider", &planetary}, + {"OrbitConstraint", &orbit_constraint}, + {"Constraint", &constraint}, + {"CameraFrame", &frame}, + {"EditableCameraFrame", &editable}, + {"AnimationCurve", &curve}}) { + if (helper->get_cls() == nullptr) { + spdlog::warn("[HavenZoom] class {} not found; skipping hook installation and keeping native zoom range", name); + return; + } + } + Il2CppClass *radius_class = 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; + } + } + if (radius_class == nullptr) { + spdlog::warn("[HavenZoom] nested class Constraint.FloatData not found; skipping hook installation and keeping " + "native zoom range"); + return; + } + IL2CppClassHelper radius(radius_class); + 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(); + if (update == nullptr || update->has_full_generic_sharing_signature) { + spdlog::warn("[HavenZoom] BlendFrameProvider.UpdateCameraFrame(UnityEngine.Camera) instance void method " + "unavailable or incompatible; skipping hook installation and keeping native zoom range"); + return; + } + if (curve_evaluate == nullptr || curve_evaluate->has_full_generic_sharing_signature) { + spdlog::warn("[HavenZoom] AnimationCurve.Evaluate(System.Single) instance float method unavailable or " + "incompatible; skipping hook installation and keeping native zoom range"); + return; + } + if (!il2cpp_class_is_assignable_from(orbit.get_cls(), planetary_provider_class)) { + spdlog::warn("[HavenZoom] PlanetaryOrbitFrameProvider no longer derives from AbstractOrbitFrameProvider; skipping " + "hook installation and keeping native zoom range"); + return; + } + struct FieldBinding { + FieldInfo **destination; + IL2CppClassHelper *owner; + const char *name, *value_type; + Il2CppClass *reference_type; + }; + const FieldBinding fields[] = { + {&blend_source, &blend, "_sourceFrameProvider", nullptr, frame_provider.get_cls()}, + {&blend_target, &blend, "_targetFrameProvider", nullptr, frame_provider.get_cls()}, + {&blend_curve, &blend, "_blendCurve", nullptr, curve.get_cls()}, + {&blend_minimum, &blend, "_softLimitMin", "System.Single", nullptr}, + {&blend_maximum, &blend, "_softLimitMax", "System.Single", nullptr}, + {&blend_ratio, &blend, "_blendRatio", "System.Single", nullptr}, + {&blend_source_frame, &blend, "_sourceFrame", nullptr, frame.get_cls()}, + {&blend_target_frame, &blend, "_targetFrame", nullptr, frame.get_cls()}, + {&blend_result_frame, &blend, "_blendedCameraFrame", nullptr, editable.get_cls()}, + {&provider_constraint, &orbit, "_constraint", nullptr, orbit_constraint.get_cls()}, + {&provider_radius, &orbit, "_radius", "System.Single", nullptr}, + {&provider_pivot, &orbit, "_orbitPivot", nullptr, target.get_cls()}, + {&provider_look_target, &orbit, "_lookTarget", nullptr, target.get_cls()}, + {&constraint_radius, &orbit_constraint, "_radius", nullptr, radius_class}, + {&radius_enabled, &radius, "_enabled", "System.Boolean", nullptr}, + {&radius_minimum, &radius, "_min", "System.Single", nullptr}, + {&radius_maximum, &radius, "_max", "System.Single", nullptr}, + {&frame_position, &frame, "_position", "UnityEngine.Vector3", nullptr}, + {&frame_rotation, &frame, "_rotation", "UnityEngine.Quaternion", nullptr}, + {&frame_fov, &frame, "_FOV", "System.Single", nullptr}, + {&frame_far_clip, &frame, "_farPlane", "System.Single", nullptr}, + {&frame_orthographic, &frame, "_orthographic", "System.Boolean", nullptr}, + }; + for (const auto &binding : fields) { + auto *field = binding.owner->GetField(binding.name).get_info(); + const char *failure = nullptr; + if (field == nullptr) + failure = "field not found"; + else if (il2cpp_field_get_flags(field) & FIELD_ATTRIBUTE_STATIC) + failure = "field became static"; + else if (field->type == nullptr || field->type->byref) + failure = "missing or by-reference field type"; + else if (binding.reference_type != nullptr) { + if (il2cpp_class_is_valuetype(binding.reference_type) + || il2cpp_class_from_type(field->type) != binding.reference_type) + failure = "incompatible reference type"; + } else if (!method_contract::Type(field->type, binding.value_type)) { + failure = "incompatible value type"; + } + if (failure != nullptr) { + auto *actual = field && field->type ? il2cpp_type_get_name(field->type) : nullptr; + auto *expected = + binding.reference_type ? il2cpp_type_get_name(il2cpp_class_get_type(binding.reference_type)) : nullptr; + spdlog::warn( + "[HavenZoom] {}.{}: {}; expected instance {}, found {}. Skipping hook installation; native zoom retained", + il2cpp_class_get_name(binding.owner->get_cls()), binding.name, failure, + expected ? expected : (binding.value_type ? binding.value_type : ""), + actual ? actual : ""); + il2cpp_free(expected); + il2cpp_free(actual); + return; + } + *binding.destination = field; + } + // [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 ad04cc49..16ab0903 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(); @@ -126,6 +127,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}},