From bf67a9bedd074ad73a3ae52207f2dd4178aa5e84 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 5 Oct 2026 23:23:05 -0500 Subject: [PATCH 1/4] 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/4] 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); From 355a6746a51e95d92da0bca1dacc16ca601bb05a Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 8 Oct 2026 18:31:55 -0500 Subject: [PATCH 3/4] Use existing helpers --- mods/src/patches/parts/haven_zoom.cc | 241 ++++++++++++++++----------- 1 file changed, 146 insertions(+), 95 deletions(-) diff --git a/mods/src/patches/parts/haven_zoom.cc b/mods/src/patches/parts/haven_zoom.cc index f2945d421..f267e4847 100644 --- a/mods/src/patches/parts/haven_zoom.cc +++ b/mods/src/patches/parts/haven_zoom.cc @@ -10,6 +10,7 @@ #include #include #include +#include void ApplyHavenRoadDepthBias(); @@ -30,43 +31,25 @@ 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)) + 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; - const auto minimum = ReadHavenField(data, radius_minimum); - const auto maximum = ReadHavenField(data, radius_maximum); - radius = ReadHavenField(provider, provider_radius); + 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; } @@ -80,35 +63,47 @@ void HavenCamera_UpdateCameraFrame_Hook(auto original, Il2CppObject *provider, C if (provider == nullptr || !std::isfinite(max_zoom) || max_zoom <= 0.0f) return; - auto *source = ReadHavenField(provider, blend_source); - auto *target = ReadHavenField(provider, blend_target); + 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; - 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) + 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; - 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); + 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; - 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); + 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; @@ -116,8 +111,7 @@ void HavenCamera_UpdateCameraFrame_Hook(auto original, Il2CppObject *provider, C // 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)) + || std::abs(af - bf) > 0.001f || source_orthographic || target_orthographic || result_orthographic) return; const auto evaluate = @@ -151,7 +145,8 @@ void HavenCamera_UpdateCameraFrame_Hook(auto original, Il2CppObject *provider, C 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); + 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); @@ -183,58 +178,114 @@ void InstallHavenZoomHooks() 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; - 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; - } + 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 = + 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(); - 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"); + 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; } - 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"); + 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)) { From 65a3b6216b4ad66ad11c89b4abb09b2092627300 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 8 Oct 2026 19:34:28 -0500 Subject: [PATCH 4/4] Identify unavailable Haven road material bindings --- mods/src/patches/parts/haven_road_depth.cc | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mods/src/patches/parts/haven_road_depth.cc b/mods/src/patches/parts/haven_road_depth.cc index 5e326c7a3..3c573fab3 100644 --- a/mods/src/patches/parts/haven_road_depth.cc +++ b/mods/src/patches/parts/haven_road_depth.cc @@ -8,6 +8,7 @@ #include #include #include +#include namespace { @@ -89,7 +90,21 @@ void ApplyHavenRoadDepthBias() if (!api.Valid()) { static bool warned = false; if (!warned) { - spdlog::warn("[HavenZoom] road material API unavailable; keeping native road depth"); + 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;