From e2f5d241b3102ee07aff447d1371a2c7ed5fa332 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 18 Sep 2026 08:09:33 -0500 Subject: [PATCH 1/4] Preview automatic warp choice on system course button --- mods/src/patches/instant_warp_policy.h | 8 + .../parts/instant_warp_confirmation.cc | 62 +++--- mods/src/patches/parts/warp_action_label.cc | 202 ++++++++++++++++++ 3 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 mods/src/patches/instant_warp_policy.h create mode 100644 mods/src/patches/parts/warp_action_label.cc diff --git a/mods/src/patches/instant_warp_policy.h b/mods/src/patches/instant_warp_policy.h new file mode 100644 index 000000000..2a1d97bae --- /dev/null +++ b/mods/src/patches/instant_warp_policy.h @@ -0,0 +1,8 @@ +#pragma once + +struct FleetPlayerData; +enum class InstantWarpConfirmation; + +// Shared by the confirmation action and its preview on the system card. +InstantWarpConfirmation ResolveInstantWarpConfirmation(FleetPlayerData* fleet); +void InstallWarpActionLabel(); diff --git a/mods/src/patches/parts/instant_warp_confirmation.cc b/mods/src/patches/parts/instant_warp_confirmation.cc index 240383416..46f8be62f 100644 --- a/mods/src/patches/parts/instant_warp_confirmation.cc +++ b/mods/src/patches/parts/instant_warp_confirmation.cc @@ -1,5 +1,6 @@ #include "config.h" #include "errormsg.h" +#include "patches/instant_warp_policy.h" #include "ship_name_match.h" #include @@ -13,7 +14,7 @@ namespace { -using PopupAction = void(CoursePromptPopupWidget*); +using PopupAction = void(CoursePromptPopupWidget*); PopupAction* initiate_regular_warp = nullptr; PopupAction* on_instant_warp_button_click = nullptr; @@ -27,46 +28,16 @@ void CoursePromptPopupViewController_AboutToShow_Hook(auto original, CoursePromp return; } - const auto& cfg = Config::Get(); - FleetPlayerData* fleet = nullptr; - if (const auto course = context->GetCourseData(); course != nullptr) { + if (const auto course = context->GetCourseData(); course != nullptr) fleet = course->PlayerFleet; - } - - std::string hull_name; - const auto candidates = ShipNameMatch::CandidateWords(fleet, &hull_name); - - const auto matches = [&candidates](const std::vector& names, bool all) -> bool { - if (all) return true; - if (candidates.empty()) return false; - return std::ranges::any_of(names, [&](const auto& configured) { - return ShipNameMatch::MatchesAny(candidates, ShipNameMatch::SplitWords(configured)); - }); - }; - - if (matches(cfg.instant_warp_always_ask, cfg.instant_warp_always_ask_all)) { - spdlog::debug("InstantWarpConfirmation: always_ask matched hull '{}', showing popup", hull_name); - return; - } - if (matches(cfg.instant_warp_auto_jump, cfg.instant_warp_auto_jump_all)) { - spdlog::debug("InstantWarpConfirmation: auto_jump matched hull '{}', selecting instant warp", hull_name); - on_instant_warp_button_click(widget); - return; - } - if (matches(cfg.instant_warp_auto_warp, cfg.instant_warp_auto_warp_all)) { - spdlog::debug("InstantWarpConfirmation: auto_warp matched hull '{}', selecting regular warp", hull_name); - initiate_regular_warp(widget); - return; - } - - switch (cfg.auto_confirm_instant_warp) { + const auto action = ResolveInstantWarpConfirmation(fleet); + spdlog::debug("InstantWarpConfirmation: resolved action {}", static_cast(action)); + switch (action) { case InstantWarpConfirmation::Warp: - spdlog::debug("InstantWarpConfirmation: default matched hull '{}', selecting regular warp", hull_name); initiate_regular_warp(widget); break; case InstantWarpConfirmation::Jump: - spdlog::debug("InstantWarpConfirmation: default matched hull '{}', selecting instant warp", hull_name); on_instant_warp_button_click(widget); break; case InstantWarpConfirmation::None: @@ -75,6 +46,24 @@ void CoursePromptPopupViewController_AboutToShow_Hook(auto original, CoursePromp } } // namespace +InstantWarpConfirmation ResolveInstantWarpConfirmation(FleetPlayerData* fleet) +{ + const auto& cfg = Config::Get(); + const auto candidates = ShipNameMatch::CandidateWords(fleet); + const auto matches = [&candidates](const std::vector& names, bool all) { + return all || (!candidates.empty() && std::ranges::any_of(names, [&](const auto& configured) { + return ShipNameMatch::MatchesAny(candidates, ShipNameMatch::SplitWords(configured)); + })); + }; + if (matches(cfg.instant_warp_always_ask, cfg.instant_warp_always_ask_all)) + return InstantWarpConfirmation::None; + if (matches(cfg.instant_warp_auto_jump, cfg.instant_warp_auto_jump_all)) + return InstantWarpConfirmation::Jump; + if (matches(cfg.instant_warp_auto_warp, cfg.instant_warp_auto_warp_all)) + return InstantWarpConfirmation::Warp; + return cfg.auto_confirm_instant_warp; +} + void InstallInstantWarpConfirmationHooks() { auto helper = CoursePromptPopupViewController::get_class_helper(); @@ -107,5 +96,6 @@ void InstallInstantWarpConfirmationHooks() return; } - SPUD_STATIC_DETOUR(about_to_show, CoursePromptPopupViewController_AboutToShow_Hook); + if (SPUD_STATIC_DETOUR(about_to_show, CoursePromptPopupViewController_AboutToShow_Hook)) + InstallWarpActionLabel(); } diff --git a/mods/src/patches/parts/warp_action_label.cc b/mods/src/patches/parts/warp_action_label.cc new file mode 100644 index 000000000..2f5dfbcf4 --- /dev/null +++ b/mods/src/patches/parts/warp_action_label.cc @@ -0,0 +1,202 @@ +#include "config.h" +#include "errormsg.h" + +#include +#include "patches/instant_warp_policy.h" +#include "patches/screen_update_hook.h" +#include "prime/DeploymentManager.h" +#include "prime/FleetsManager.h" +#include "prime/StarNodeObjectViewerWidget.h" +#include "str_utils.h" +#include + +#include +#include +#include + +namespace +{ +// This is a client of the existing frame dispatcher, not another UI detour. +// Never mutate the button context, timer, resource cost, or click handler. +Il2CppObject* Invoke(Il2CppObject* object, const char* name, int count = 0, void** args = nullptr) +{ + if (!object) + return nullptr; + auto* method = il2cpp_class_get_method_from_name(object->klass, name, count); + if (!method || !method->invoker_method || (method->flags & METHOD_ATTRIBUTE_STATIC) || !method->return_type + || method->return_type->byref) + throw std::runtime_error("missing instance method"); + for (int i = 0; i < count; ++i) { + auto* parameter = method->parameters[i]; + auto* expected = parameter ? il2cpp_class_from_type(parameter) : nullptr; + auto* argument = args ? static_cast(args[i]) : nullptr; + if (!expected || parameter->byref || il2cpp_class_is_valuetype(expected) + || (argument && !il2cpp_class_is_assignable_from(expected, argument->klass))) + throw std::runtime_error("reference argument contract changed"); + } + Il2CppException* exception = nullptr; + auto* result = il2cpp_runtime_invoke(method, object, args, &exception); + if (exception) + throw std::runtime_error("managed invocation failed"); + return result; +} + +bool Boolean(Il2CppObject* result) +{ + if (!result || il2cpp_class_get_type(result->klass)->type != IL2CPP_TYPE_BOOLEAN) + throw std::runtime_error("expected Boolean"); + return *static_cast(il2cpp_object_unbox(result)); +} + +FieldInfo* Field(Il2CppObject* object, const char* name, const char* expected) +{ + auto* field = object ? il2cpp_class_get_field_from_name(object->klass, name) : nullptr; + auto* type = field ? il2cpp_class_from_type(field->type) : nullptr; + if (!field || (field->type->attrs & FIELD_ATTRIBUTE_STATIC) || !type || std::strcmp(type->name, expected) != 0) + throw std::runtime_error("field contract changed"); + return field; +} + +Il2CppObject* Read(Il2CppObject* object, const char* name, const char* expected) +{ + if (!object) + return nullptr; + auto* field = Field(object, name, expected); + if (field->type->byref || il2cpp_class_is_valuetype(il2cpp_class_from_type(field->type))) + throw std::runtime_error("expected reference field"); + return il2cpp_field_get_value_object(field, object); +} + +bool Flag(Il2CppObject* object, const char* name) +{ return object && Boolean(il2cpp_field_get_value_object(Field(object, name, "Boolean"), object)); } + +bool Alive(Il2CppObject* object) +{ + if (!object) + return false; + static auto helper = il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine", "Object"); + static auto* method = helper.GetMethodInfo("op_Implicit", 1); + if (!method) + return false; + void* args[] = {object}; + Il2CppException* exception = nullptr; + auto* result = il2cpp_runtime_invoke(method, nullptr, args, &exception); + return !exception && Boolean(result); +} + +std::string CurrentText(Il2CppObject* label) +{ + auto* text = Invoke(label, "CurrentText"); + if (!text || il2cpp_class_get_type(text->klass)->type != IL2CPP_TYPE_STRING) + throw std::runtime_error("expected label text"); + return to_string(reinterpret_cast(text)); +} + +struct OwnedLabel { + Il2CppGCHandle handle = nullptr; + std::string text; + + Il2CppObject* Get() const + { return handle ? il2cpp_gchandle_get_target(handle) : nullptr; } + + void Clear() + { + auto* label = Get(); + auto previous = std::move(text); + if (handle) + il2cpp_gchandle_free(handle); + handle = nullptr; + text.clear(); + // Forget ownership even if native cleanup fails. + if (Alive(label) && Flag(label, "_textOverride") && CurrentText(label) == previous) + Invoke(label, "ClearTextOverride"); + } + + void Apply(Il2CppObject* label, const char* desired) + { + if (!label || !desired) { + Clear(); + return; + } + if (Get() != label) { + Clear(); + // Don't take ownership from an existing text override. + if (Flag(label, "_textOverride")) + return; + handle = il2cpp_gchandle_new_weakref(label, false); + if (!handle) + return; + } + const auto current = CurrentText(label); + if (!text.empty() && Flag(label, "_textOverride") && current != text) { + Clear(); + return; + } + if (text == desired && Flag(label, "_textOverride") && current == desired) + return; + auto* value = il2cpp_string_new(desired); + if (!value) + return; + void* args[] = {value}; + Invoke(label, "OverrideLocalizedText", 1, args); + text = desired; + } +}; +OwnedLabel action_label; + +void Update() +{ + static auto next = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + if (now < next) + return; + next = now + std::chrono::milliseconds(100); + try { + auto* widget = reinterpret_cast(ObjectFinder::Get()); + const bool active = Alive(widget) && Boolean(Invoke(widget, "get_isActiveAndEnabled")); + auto* button = active ? Read(widget, "_visitButton", "GenericButtonWidget") : nullptr; + const bool enabled = Alive(button) && Boolean(Invoke(button, "get_isActiveAndEnabled")) + && Boolean(Invoke(button, "get_Interactable")); + const char* desired = nullptr; + // The ordinary visit path uses the currently selected fleet. The card's + // cached popup CourseData is populated on click, so it is not a preview source. + // Toll/wormhole buttons have separate confirmation paths and retain native text. + if (enabled) { + auto* fleets = reinterpret_cast(FleetsManager::Instance()); + auto* deployment = reinterpret_cast(DeploymentManger::Instance()); + auto* fleet = Alive(fleets) ? Invoke(fleets, "GetSelectedFleetData") : nullptr; + void* args[] = {fleet}; + if (fleet && Alive(deployment) && Boolean(Invoke(deployment, "HasInstantWarpAbility", 1, args))) { + switch (ResolveInstantWarpConfirmation(reinterpret_cast(fleet))) { + case InstantWarpConfirmation::Warp: + desired = "WARP"; + break; + case InstantWarpConfirmation::Jump: + desired = "JUMP"; + break; + case InstantWarpConfirmation::None: + break; + } + } + } + auto* label = desired ? Read(button, "_mainText", "TextLocalizer") : nullptr; + action_label.Apply(Alive(label) ? label : nullptr, desired); + } catch (const std::exception& error) { + static bool warned = false; + if (!warned) { + warned = true; + spdlog::warn("[WarpActionLabel] native label unavailable: {}", error.what()); + } + try { + action_label.Clear(); + } catch (...) { + } + } +} +} // namespace + +void InstallWarpActionLabel() +{ + if (install_screen_manager_update_hook() && register_screen_manager_update_callback(Update)) + spdlog::info("[WarpActionLabel] system-card action labels enabled"); +} From fd15fb53245172579e570af433c892074434d1e5 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 20:44:10 -0500 Subject: [PATCH 2/4] Add shared IL2CPP runtime boundary helpers with contract tests --- .github/workflows/ci.yaml | 12 +++ mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 59 +++++++++++++++ tests/il2cpp_runtime.cc | 127 ++++++++++++++++++++++++++++++++ xmake.lua | 11 +++ 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 mods/src/il2cpp/runtime.h create mode 100644 tests/il2cpp_runtime.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..ff6de6ff7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + xmake run il2cpp-runtime-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h new file mode 100644 index 000000000..8617cedb1 --- /dev/null +++ b/mods/src/il2cpp/runtime.h @@ -0,0 +1,59 @@ +#pragma once + +#include "il2cpp_helper.h" +#include + +// Mechanics shared by optional runtime features. Signature/overload selection, +// argument storage and feature-specific failure policy remain with the caller. +namespace Il2CppRuntime +{ +inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) +{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } + +inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) +{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } + +inline bool Type(const Il2CppType* type, int expected) +{ return type && !type->byref && type->type == expected; } + +inline bool Reference(const Il2CppType* type) +{ + return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) + || Type(type, IL2CPP_TYPE_STRING); +} + +inline bool Instance(const MethodInfo* method, int count, int result) +{ + return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) + && method->parameters_count == count && Type(method->return_type, result) + && !method->has_full_generic_sharing_signature; +} + +// The method and argument ABI must already be established by the caller. +// IL2CPP takes references directly, but value/byref arguments as addresses. +// A null target is valid for static methods. Success is independent of whether +// the return value is null (including void methods). Outputs change on success only. +inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2CppObject** result = nullptr) +{ + if (!method) + return false; + Il2CppException* exception = nullptr; + auto* value = il2cpp_runtime_invoke(method, target, args, &exception); + if (exception) + return false; + if (result) + *result = value; + return true; +} + +inline bool TryBoolean(Il2CppObject* boxed, bool& value) +{ + if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + return false; + auto* data = static_cast(il2cpp_object_unbox(boxed)); + if (!data) + return false; + value = *data; + return true; +} +} // namespace Il2CppRuntime diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc new file mode 100644 index 000000000..1b7113340 --- /dev/null +++ b/tests/il2cpp_runtime.cc @@ -0,0 +1,127 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/runtime.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppType type{}; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3, calls = 0; +bool fail = false, boxed = false, null_result = false; +void* seen_target = nullptr; +void** seen_args = nullptr; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) +{ return &type; } +END_API +API(void*, il2cpp_object_unbox, (Il2CppObject*)) +{ return &boxed; } +END_API +API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) +{ + ++calls; + seen_target = target; + seen_args = args; + if (fail) + *error = reinterpret_cast(&object); + return null_result ? nullptr : &object; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("runtime helper regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); + Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); + Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); + Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); + + Il2CppObject* result = &object; + Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); + bool value = true; + void* args[] = {&value, &object, nullptr}; + Require(Il2CppRuntime::TryInvoke(&method, &object, args, &result)); + Require(seen_target == &object && seen_args == args && seen_args[0] == &value && seen_args[1] == &object); + // Static calls, null reference/void returns and exceptions remain distinct. + null_result = true; + Require(Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && !result && !seen_target); + result = &object; + fail = true; + Require(!Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && result == &object); + + object.klass = &klass; + type.type = IL2CPP_TYPE_BOOLEAN; + Require(Il2CppRuntime::TryBoolean(&object, value) && !value); + boxed = true; + Require(Il2CppRuntime::TryBoolean(&object, value) && value); + type.type = IL2CPP_TYPE_I4; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + Require(!Il2CppRuntime::TryBoolean(nullptr, value)); + type.type = IL2CPP_TYPE_CLASS; + Require(Il2CppRuntime::Reference(&type)); + type.byref = true; + Require(!Il2CppRuntime::Reference(&type)); + type.byref = false; + type.type = IL2CPP_TYPE_VOID; + method.methodPointer = reinterpret_cast(1); + method.invoker_method = reinterpret_cast(1); + method.return_type = &type; + Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = METHOD_ATTRIBUTE_STATIC; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = 0; + method.has_full_generic_sharing_signature = true; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + std::cout << "IL2CPP runtime helper regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..ac08005a2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-runtime-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_runtime.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end From 733997cfe2cd34c9203b61cff755046ca9937643 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:27:13 -0500 Subject: [PATCH 3/4] Narrow shared invocation helpers around existing loading screens --- mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 27 ++----- .../src/patches/parts/loading_screen_common.h | 16 ++--- tests/il2cpp_runtime.cc | 70 ++++--------------- 4 files changed, 28 insertions(+), 91 deletions(-) diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 19708b2cd..3d4ff6e3f 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; - auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; + auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); + auto image = il2cpp_assembly_get_image(assemblyT); - auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; + auto cls = il2cpp_class_from_name(image, namespacez, name); return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h index 8617cedb1..b3043673e 100644 --- a/mods/src/il2cpp/runtime.h +++ b/mods/src/il2cpp/runtime.h @@ -7,28 +7,6 @@ // argument storage and feature-specific failure policy remain with the caller. namespace Il2CppRuntime { -inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) -{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } - -inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) -{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } - -inline bool Type(const Il2CppType* type, int expected) -{ return type && !type->byref && type->type == expected; } - -inline bool Reference(const Il2CppType* type) -{ - return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) - || Type(type, IL2CPP_TYPE_STRING); -} - -inline bool Instance(const MethodInfo* method, int count, int result) -{ - return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) - && method->parameters_count == count && Type(method->return_type, result) - && !method->has_full_generic_sharing_signature; -} - // The method and argument ABI must already be established by the caller. // IL2CPP takes references directly, but value/byref arguments as addresses. // A null target is valid for static methods. Success is independent of whether @@ -48,7 +26,10 @@ inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2Cp inline bool TryBoolean(Il2CppObject* boxed, bool& value) { - if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + if (!boxed || !boxed->klass) + return false; + const auto* type = il2cpp_class_get_type(boxed->klass); + if (!type || type->byref || type->type != IL2CPP_TYPE_BOOLEAN) return false; auto* data = static_cast(il2cpp_object_unbox(boxed)); if (!data) diff --git a/mods/src/patches/parts/loading_screen_common.h b/mods/src/patches/parts/loading_screen_common.h index 071b63976..4f78890c7 100644 --- a/mods/src/patches/parts/loading_screen_common.h +++ b/mods/src/patches/parts/loading_screen_common.h @@ -3,7 +3,7 @@ #include "config.h" #include "errormsg.h" -#include +#include #include #include @@ -28,9 +28,8 @@ struct FakeColor { float r, g, b, a; }; inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return nullptr; - Il2CppException* exception = nullptr; - Il2CppObject* result = il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + Il2CppObject* result = nullptr; + if (!Il2CppRuntime::TryInvoke(method, target, args, &result)) { spdlog::warn("[LS] {} invocation failed", name); return nullptr; } @@ -40,9 +39,7 @@ inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void* inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return false; - Il2CppException* exception = nullptr; - il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + if (!Il2CppRuntime::TryInvoke(method, target, args)) { spdlog::warn("[LS] {} invocation failed", name); return false; } @@ -52,9 +49,8 @@ inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, cons inline bool InvokeBool(const MethodInfo* method, void* target, void** args, const char* name) { Il2CppObject* result = InvokeRuntime(method, target, args, name); - if (!result) return false; - void* value = il2cpp_object_unbox(result); - return value ? *reinterpret_cast(value) : false; + bool value = false; + return Il2CppRuntime::TryBoolean(result, value) && value; } inline int32_t InvokeInt32(const MethodInfo* method, void* target, int32_t fallback, const char* name) diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc index 1b7113340..b098a2bbc 100644 --- a/tests/il2cpp_runtime.cc +++ b/tests/il2cpp_runtime.cc @@ -15,8 +15,8 @@ Il2CppClass klass; Il2CppType type{}; Il2CppObject object{}; MethodInfo method{}; -int stage = 3, calls = 0; -bool fail = false, boxed = false, null_result = false; +int calls = 0; +bool fail = false, boxed = false, null_result = false, null_type = false, null_unbox = false; void* seen_target = nullptr; void** seen_args = nullptr; } // namespace @@ -27,42 +27,11 @@ void** seen_args = nullptr; #define API(ret, name, params) name##_t name = +[] params->ret #define END_API ; #endif -API(Il2CppDomain*, il2cpp_domain_get, ()) -{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } -END_API -API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) -{ - if (!domain) - std::abort(); - return stage >= 1 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) -{ - if (!assembly) - std::abort(); - return stage >= 2 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) -{ - if (!image) - std::abort(); - return stage >= 3 ? &klass : nullptr; -} -END_API -API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) -{ - if (!cls) - std::abort(); - return &method; -} -END_API API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) -{ return &type; } +{ return null_type ? nullptr : &type; } END_API API(void*, il2cpp_object_unbox, (Il2CppObject*)) -{ return &boxed; } +{ return null_unbox ? nullptr : &boxed; } END_API API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) { @@ -81,12 +50,6 @@ void Require(bool condition) } int main() { - for (stage = -1; stage < 3; ++stage) - Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); - Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); - Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); - Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); - Il2CppObject* result = &object; Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); bool value = true; @@ -108,20 +71,17 @@ int main() type.type = IL2CPP_TYPE_I4; Require(!Il2CppRuntime::TryBoolean(&object, value) && value); Require(!Il2CppRuntime::TryBoolean(nullptr, value)); - type.type = IL2CPP_TYPE_CLASS; - Require(Il2CppRuntime::Reference(&type)); + type.type = IL2CPP_TYPE_BOOLEAN; type.byref = true; - Require(!Il2CppRuntime::Reference(&type)); - type.byref = false; - type.type = IL2CPP_TYPE_VOID; - method.methodPointer = reinterpret_cast(1); - method.invoker_method = reinterpret_cast(1); - method.return_type = &type; - Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = METHOD_ATTRIBUTE_STATIC; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = 0; - method.has_full_generic_sharing_signature = true; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + type.byref = false; + null_type = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_type = false; + null_unbox = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_unbox = false; + object.klass = nullptr; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); std::cout << "IL2CPP runtime helper regressions passed\n"; } From d6c10b7d0d0988d4438c488cfa9354524d820106 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:28:55 -0500 Subject: [PATCH 4/4] Guard existing IL2CPP class lookup when metadata is unavailable --- .github/workflows/ci.yaml | 12 ++++++ mods/src/il2cpp/il2cpp_helper.h | 6 +-- tests/il2cpp_class_lookup.cc | 71 +++++++++++++++++++++++++++++++++ xmake.lua | 11 +++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/il2cpp_class_lookup.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..025c4356b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + xmake run il2cpp-class-lookup-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/tests/il2cpp_class_lookup.cc b/tests/il2cpp_class_lookup.cc new file mode 100644 index 000000000..0d471ea00 --- /dev/null +++ b/tests/il2cpp_class_lookup.cc @@ -0,0 +1,71 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/il2cpp_helper.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("class lookup regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls()); + Require(il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls() == &klass); + Require(!IL2CppClassHelper(nullptr).GetMethodInfo("Method", 0)); + Require(IL2CppClassHelper(&klass).GetMethodInfo("Method", 0) == &method); + + std::cout << "IL2CPP class lookup regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..cfbefafff 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-class-lookup-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_class_lookup.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end