From 5d033c255ff78663094c5d315c45152c9a491415 Mon Sep 17 00:00:00 2001 From: kcid Date: Sat, 24 Oct 2026 01:40:29 -0700 Subject: [PATCH] fix: restore station-exterior zoom after interior visit (interior_zoom) The interior zoom-out hook keyed its native-radius capture and restore by the OrbitFrameProvider instance. That provider is recreated across scene/section transitions, but the Constraint.FloatData radius object it drives persists and is shared with the exterior station camera. When a widened provider was torn down before it next ticked outside the interior, the widened radius was inherited by a freshly created exterior provider the hook had never captured, so the restore branch (find(provider) miss) never fired and the exterior stayed pinned at scale*native: zoomed out and un-zoomable. Key the capture and restore by the radius object instead of the provider, so any provider that later presents that radius restores it to native on its first exterior frame (and a stuck state self-heals). The map key stays compare-only and the stored value is plain floats, so no stale-object access is introduced. Follow-up to #332. --- mods/src/patches/parts/interior_zoom.cc | 41 ++++++++++--------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/mods/src/patches/parts/interior_zoom.cc b/mods/src/patches/parts/interior_zoom.cc index f782bf55a..d849742dd 100644 --- a/mods/src/patches/parts/interior_zoom.cc +++ b/mods/src/patches/parts/interior_zoom.cc @@ -12,20 +12,12 @@ #include #include -// Station-interior zoom-out. -// -// The interior view is a BlendFrameProvider between a StaticFrameProvider (zoomed-in) and an -// OrbitFrameProvider pinned at a fixed radius (zoomed-out, min==max); zoom slides the blend ratio, so -// the far stop is that orbit endpoint's radius. We push that pinned radius outward to extend zoom-out. -// That same OrbitFrameProvider instance IS the exterior station camera (the interior's fully-zoomed-out -// end is the exterior view), so the hook widens it only while the section is Starbase_Interior and -// restores the native radius otherwise -- leaving the exterior view (and its component-level labels) -// stock. See fix_pan.cc, which drives the same provider for the exterior arrow-key rotate. -// -// graphics.interior_zoom_scale is a multiplier of the native endpoint (1.0 = native), not an absolute -// distance like zoom/haven_zoom: the native stop is a single pinned radius, so a factor is -// resolution-independent and self-heals if the game changes that radius. The config-knob and -// Constraint.FloatData field-resolution pattern follow haven_zoom.cc. +// Station-interior zoom-out: the interior view's far stop is an OrbitFrameProvider pinned at a fixed +// radius that is also the exterior station camera, so the hook pushes that radius out while the section +// is Starbase_Interior and restores native otherwise (see fix_pan.cc, which drives the same provider). +// graphics.interior_zoom_scale multiplies that native radius (1.0 = native) rather than setting an +// absolute distance like zoom/haven_zoom, so it stays resolution-independent; field resolution follows +// haven_zoom.cc. namespace { FieldInfo *provider_constraint = nullptr; // AbstractOrbitFrameProvider._constraint -> OrbitConstraint @@ -38,9 +30,11 @@ bool fields_ok = false; struct NativeRange { float min, max; }; -// Native (pre-widen) radius range per live interior OrbitFrameProvider. The pointer is ONLY a map key, -// never dereferenced from the stored entry, so a retired scene instance cannot cause a stale-object -// access. Keyed by provider; the interior has a single orbit endpoint so this stays tiny. +// Native (pre-widen) radius range, keyed by the Constraint.FloatData radius object, NOT the provider: +// the provider is recreated across transitions while the radius it drives persists and is shared with the +// exterior camera, so provider-keying stranded the exterior at a widened radius it never restored. The +// pointer is only a map key (the stored value is plain floats), so a retired object cannot cause a +// stale-object access. std::unordered_map g_native; bool g_reported = false; @@ -73,13 +67,13 @@ void OrbitFrameProvider_UpdateConstrains_Hook(auto original, Il2CppObject *provi const float scale = InteriorScale(); if (in_interior && scale > 1.0f) { - // Capture the native range once (before any widening), keyed by this provider. - auto it = g_native.find(provider); + // Capture the native range once (before any widening), keyed by this radius object. + auto it = g_native.find(radius); if (it == g_native.end()) { NativeRange nr{0.0f, 0.0f}; il2cpp_field_get_value(radius, radius_min, &nr.min); il2cpp_field_get_value(radius, radius_max, &nr.max); - it = g_native.emplace(provider, nr).first; + it = g_native.emplace(radius, nr).first; if (!g_reported) { bool enabled = false; if (radius_enabled) { @@ -99,10 +93,9 @@ void OrbitFrameProvider_UpdateConstrains_Hook(auto original, Il2CppObject *provi il2cpp_field_set_value(radius, radius_min, &new_min); il2cpp_field_set_value(radius, radius_max, &new_max); } else { - // Not the interior (or feature off): the far endpoint is SHARED with the exterior station view, so - // restore the native radius on any provider we previously widened. Without this the exterior stays - // zoomed out and its component-level labels drop out of LOD range. - auto it = g_native.find(provider); + // Not the interior (or feature off): the far endpoint is SHARED with the exterior view, so restore + // native on any radius we widened, else the exterior stays zoomed out and its labels drop out of LOD. + auto it = g_native.find(radius); if (it != g_native.end()) { float native_min = it->second.min, native_max = it->second.max; il2cpp_field_set_value(radius, radius_min, &native_min);