From 829d3ca136eae2c9dfc8b43267972745ec3a2d1d Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sun, 25 Oct 2026 06:41:14 -0500 Subject: [PATCH 1/3] Resolve anomaly HUD owners through their registered canvases --- .../patches/parts/galactic_anomaly_timer.cc | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/mods/src/patches/parts/galactic_anomaly_timer.cc b/mods/src/patches/parts/galactic_anomaly_timer.cc index 211074680..7d5cf0e11 100644 --- a/mods/src/patches/parts/galactic_anomaly_timer.cc +++ b/mods/src/patches/parts/galactic_anomaly_timer.cc @@ -13,6 +13,7 @@ #include #include #include +#include namespace { @@ -153,6 +154,7 @@ struct Root { }; Root navigation, panel, label, anomalyManager; +std::string renderedText; void Clear() { @@ -166,6 +168,7 @@ void Clear() panel.reset(); navigation.reset(); anomalyManager.reset(); + renderedText.clear(); } bool InSystemNavigation() @@ -178,27 +181,14 @@ bool InSystemNavigation() && static_cast(section) == SectionID::Navigation_System; } -Il2CppObject* NavigationHud() -{ - static auto* screen = Class("Assembly-CSharp", "Digit.Client.UI", "ScreenManager"); - static auto* top = IL2CppClassHelper(screen).GetMethodInfo("GetTopCanvas", 1); - bool visibleOnly = true; - void* args[] = {&visibleOnly}; - Il2CppObject* canvas = nullptr; - if (!TryInvoke(top, nullptr, args, &canvas) || !Alive(canvas)) - return nullptr; - static auto* hud = Class("Assembly-CSharp", "Digit.Prime.Navigation.UI", "HudNavigationViewController"); - return WithType(TypeMethod(canvas->klass, "GetComponentInChildren"), canvas, hud); -} - -Il2CppObject* ExistingAnomalyManager(Il2CppClass* managerClass) +Il2CppObject* ExistingSingleton(Il2CppClass* singletonClass) { // get_Instance may search the scene or create a replacement. Read only the // native owner's existing singleton, and retry when it has not initialized. - static auto* parent = managerClass ? il2cpp_class_get_parent(managerClass) : nullptr; + auto* parent = singletonClass ? il2cpp_class_get_parent(singletonClass) : nullptr; if (!parent || std::strcmp(il2cpp_class_get_name(parent), "MonoSingleton`1")) return nullptr; - static auto* instance = il2cpp_class_get_field_from_name(parent, "s_instance"); + auto* instance = il2cpp_class_get_field_from_name(parent, "s_instance"); if (!instance || !(il2cpp_field_get_flags(instance) & FIELD_ATTRIBUTE_STATIC) || !instance->type || il2cpp_type_is_byref(instance->type)) return nullptr; @@ -207,7 +197,43 @@ Il2CppObject* ExistingAnomalyManager(Il2CppClass* managerClass) return nullptr; Il2CppObject* value = nullptr; il2cpp_field_static_get_value(instance, &value); - return value && il2cpp_class_is_assignable_from(managerClass, value->klass) && Alive(value) ? value : nullptr; + return value && il2cpp_class_is_assignable_from(singletonClass, value->klass) && Alive(value) ? value : nullptr; +} + +Il2CppObject* RegisteredController(Il2CppClass* controllerClass, Il2CppClass* contextClass) +{ + if (!controllerClass || !contextClass) + return nullptr; + static auto* screenClass = Class("Assembly-CSharp", "Digit.Client.UI", "ScreenManager"); + auto* screens = Field(ExistingSingleton(screenClass), "_screens"); + int count = 0; + if (!Value(screens, "get_Count", count) || count < 0 || count > 256) + return nullptr; + auto* item = IL2CppClassHelper(screens->klass).GetMethodInfo("get_Item", 1); + for (int index = 0; index < count; ++index) { + void* args[] = {&index}; + Il2CppObject* canvas = nullptr; + if (!TryInvoke(item, screens, args, &canvas) || !Alive(canvas)) + continue; + auto* context = Get(canvas, "get_Context"); + bool visible = false; + if (!context || !il2cpp_class_is_assignable_from(contextClass, context->klass) + || !Value(canvas, "get_Visible", visible) || !visible) + continue; + // Search only the registered canvas that owns this context. The top + // interaction canvas and its root can belong to unrelated overlays. + auto* controller = WithType(TypeMethod(canvas->klass, "GetComponentInChildren"), canvas, controllerClass); + if (Alive(controller)) + return controller; + } + return nullptr; +} + +Il2CppObject* NavigationHud() +{ + static auto* hud = Class("Assembly-CSharp", "Digit.Prime.Navigation.UI", "HudNavigationViewController"); + static auto* context = Class("Assembly-CSharp", "Digit.Prime.Navigation", "NavigationViewData"); + return RegisteredController(hud, context); } Il2CppObject* Component(Il2CppObject* object, Il2CppClass* type) @@ -311,9 +337,9 @@ bool Create(Il2CppObject* nav) Il2CppObject* name = nullptr; if (!TryInvoke(IL2CppClassHelper(names->klass).GetMethodInfo("get_Item", 1), names, args, &name) || !Alive(name)) return false; - static auto* drawerClass = Class("Assembly-CSharp", "Digit.Prime.HUD", "HudAllianceAndNewsViewController"); - auto* uiRoot = Get(Get(nav, "get_transform"), "get_root"); - auto* drawer = uiRoot ? WithType(TypeMethod(uiRoot->klass, "GetComponentInChildren"), uiRoot, drawerClass) : nullptr; + static auto* drawerClass = Class("Assembly-CSharp", "Digit.Prime.HUD", "HudAllianceAndNewsViewController"); + static auto* drawerContext = Class("Assembly-CSharp", "Digit.Prime.HUD", "HudAllianceAndNewsViewControllerContext"); + auto* drawer = RegisteredController(drawerClass, drawerContext); auto* chest = Get(Field(drawer, "_chestPromotion"), "get_transform"); auto* parent = Get(chest, "get_parent"); static auto* tmp = Class("Unity.TextMeshPro", "TMPro", "TextMeshProUGUI"); @@ -381,8 +407,9 @@ void Update() Clear(); return; } - bool active = false; - if (!Alive(navigation.get()) || !Value(navigation.get(), "get_isActiveAndEnabled", active) || !active) { + static auto nextDiscovery = std::chrono::steady_clock::time_point{}; + if (!Alive(navigation.get()) && now >= nextDiscovery) { + nextDiscovery = now + std::chrono::seconds(1); Clear(); navigation.reset(NavigationHud()); } @@ -397,7 +424,7 @@ void Update() // Cache the Unity object, not its timer data. Unity's liveness check detects // destruction even while the managed wrapper is retained by our GC handle. if (!Alive(anomalyManager.get())) - anomalyManager.reset(ExistingAnomalyManager(managerClass)); + anomalyManager.reset(ExistingSingleton(managerClass)); auto* manager = anomalyManager.get(); Il2CppObject* anomaly = nullptr; void* args[] = {&system}; @@ -431,7 +458,11 @@ void Update() auto seconds = ticks / 10000000 + (ticks % 10000000 != 0); auto text = seconds >= 3600 ? fmt::format("{}h {:02}m {:02}s", seconds / 3600, seconds / 60 % 60, seconds % 60) : fmt::format("{:02}m {:02}s", seconds / 60, seconds % 60); - show = Set(label.get(), "set_text", il2cpp_string_new(text.c_str())); + if (text != renderedText) { + show = Set(label.get(), "set_text", il2cpp_string_new(text.c_str())); + if (show) + renderedText = std::move(text); + } } Set(panel.get(), "SetActive", &show); } From 04b2e4e788985edd95e4bcbe19ec16688d681aa5 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sun, 25 Oct 2026 06:52:18 -0500 Subject: [PATCH 2/3] Retain inactive owner recovery and cache whole displayed seconds --- .../patches/parts/galactic_anomaly_timer.cc | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/mods/src/patches/parts/galactic_anomaly_timer.cc b/mods/src/patches/parts/galactic_anomaly_timer.cc index 7d5cf0e11..2f9cf5bf5 100644 --- a/mods/src/patches/parts/galactic_anomaly_timer.cc +++ b/mods/src/patches/parts/galactic_anomaly_timer.cc @@ -13,7 +13,6 @@ #include #include #include -#include namespace { @@ -154,7 +153,7 @@ struct Root { }; Root navigation, panel, label, anomalyManager; -std::string renderedText; +std::int64_t renderedSeconds = -1; void Clear() { @@ -168,7 +167,7 @@ void Clear() panel.reset(); navigation.reset(); anomalyManager.reset(); - renderedText.clear(); + renderedSeconds = -1; } bool InSystemNavigation() @@ -408,10 +407,15 @@ void Update() return; } static auto nextDiscovery = std::chrono::steady_clock::time_point{}; - if (!Alive(navigation.get()) && now >= nextDiscovery) { + bool active = false; + if ((!Alive(navigation.get()) || !Value(navigation.get(), "get_isActiveAndEnabled", active) || !active) + && now >= nextDiscovery) { nextDiscovery = now + std::chrono::seconds(1); - Clear(); - navigation.reset(NavigationHud()); + auto* replacement = NavigationHud(); + if ((replacement && replacement != navigation.get()) || !Alive(navigation.get())) { + Clear(); + navigation.reset(replacement); + } } auto* nav = navigation.get(); bool show = false; @@ -456,12 +460,12 @@ void Update() return; if (show) { auto seconds = ticks / 10000000 + (ticks % 10000000 != 0); - auto text = seconds >= 3600 ? fmt::format("{}h {:02}m {:02}s", seconds / 3600, seconds / 60 % 60, seconds % 60) - : fmt::format("{:02}m {:02}s", seconds / 60, seconds % 60); - if (text != renderedText) { + if (seconds != renderedSeconds) { + auto text = seconds >= 3600 ? fmt::format("{}h {:02}m {:02}s", seconds / 3600, seconds / 60 % 60, seconds % 60) + : fmt::format("{:02}m {:02}s", seconds / 60, seconds % 60); show = Set(label.get(), "set_text", il2cpp_string_new(text.c_str())); if (show) - renderedText = std::move(text); + renderedSeconds = seconds; } } Set(panel.get(), "SetActive", &show); From 63496189c1c4a693d832f176b714c23475864ca4 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sun, 25 Oct 2026 08:48:20 -0500 Subject: [PATCH 3/3] Invoke the non-generic anomaly owner component overload --- mods/src/patches/parts/galactic_anomaly_timer.cc | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mods/src/patches/parts/galactic_anomaly_timer.cc b/mods/src/patches/parts/galactic_anomaly_timer.cc index 2f9cf5bf5..19e65c399 100644 --- a/mods/src/patches/parts/galactic_anomaly_timer.cc +++ b/mods/src/patches/parts/galactic_anomaly_timer.cc @@ -41,15 +41,20 @@ bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2CppObject } // Unity has Type/string and generic overloads with the same argument count. -const MethodInfo* TypeMethod(Il2CppClass* cls, const char* name) +const MethodInfo* TypeMethod(Il2CppClass* cls, const char* name, int argumentCount = 1) { // The shared filtered lookup cannot inspect is_generic and only searches // one class. Keep the overload check here so a generic overload cannot win. void* iter = nullptr; while (cls) { while (auto* method = il2cpp_class_get_methods(cls, &iter)) { - if (method->is_generic || std::strcmp(method->name, name) || method->parameters_count != 1) + if (method->is_generic || std::strcmp(method->name, name) || method->parameters_count != argumentCount) continue; + if (argumentCount == 2) { + auto* second = il2cpp_method_get_param(method, 1); + if (!second || il2cpp_type_is_byref(second) || il2cpp_type_get_type(second) != IL2CPP_TYPE_BOOLEAN) + continue; + } auto* param = il2cpp_class_from_type(il2cpp_method_get_param(method, 0)); if (param && !std::strcmp(il2cpp_class_get_namespace(param), "System") && !std::strcmp(il2cpp_class_get_name(param), "Type")) @@ -221,7 +226,12 @@ Il2CppObject* RegisteredController(Il2CppClass* controllerClass, Il2CppClass* co continue; // Search only the registered canvas that owns this context. The top // interaction canvas and its root can belong to unrelated overlays. - auto* controller = WithType(TypeMethod(canvas->klass, "GetComponentInChildren"), canvas, controllerClass); + // Component exposes the non-generic (Type, bool) overload. The one-argument + // overload is generic and cannot be invoked with a runtime Type object. + bool includeInactive = false; + void* controllerArgs[] = {il2cpp_type_get_object(il2cpp_class_get_type(controllerClass)), &includeInactive}; + Il2CppObject* controller = nullptr; + TryInvoke(TypeMethod(canvas->klass, "GetComponentInChildren", 2), canvas, controllerArgs, &controller); if (Alive(controller)) return controller; }