Skip to content
Merged
34 changes: 34 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ 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: 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

Expand All @@ -203,6 +217,11 @@ jobs:
"build/windows/x64/release/stfc-community-mod.dll" `
"version.dll"

- name: Test checked IL2CPP helpers
run: |
xmake build -y il2cpp-checked-tests
xmake run il2cpp-checked-tests

- name: Verify DLL provenance
shell: pwsh
run: |
Expand Down Expand Up @@ -469,6 +488,16 @@ 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: 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
Expand All @@ -489,6 +518,11 @@ jobs:
echo "::warning::Swift module cache directory was not created"
fi

- name: Test checked IL2CPP helpers
run: |
xmake build -y il2cpp-checked-tests
xmake run il2cpp-checked-tests

- name: Verify portable macOS linkage
shell: bash
run: |
Expand Down
70 changes: 70 additions & 0 deletions mods/src/il2cpp/il2cpp_checked.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma once

#include "runtime.h"
#include <functional>
#include <il2cpp-tabledefs.h>

#include <cstring>
#include <stdexcept>

// Optional UI features use these checked operations to stop on metadata drift
// or managed exceptions. Arguments are managed references (including null), not
// value types or byrefs. Existing unchecked helper behavior is unchanged.
namespace Il2CppChecked
{
inline Il2CppObject* Invoke(Il2CppObject* object, const char* name, int count = 0, void** args = nullptr)
{
if (!object)
return nullptr;
if (!object->klass || count < 0 || (count && !args))
throw std::runtime_error("invalid instance invocation");
auto* method = IL2CppClassHelper(object->klass).GetMethodInfo(name, count);
if (!method || method->parameters_count != count || !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 ? method->parameters[i] : nullptr;
auto* expected = parameter ? il2cpp_class_from_type(parameter) : nullptr;
auto* argument = args ? static_cast<Il2CppObject*>(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");
}
Il2CppObject* result = nullptr;
if (!Il2CppRuntime::TryInvoke(method, object, args, &result))
throw std::runtime_error("managed invocation failed");
return result;
}

inline bool Boolean(Il2CppObject* result)
{
bool value = false;
if (!Il2CppRuntime::TryBoolean(result, value))
throw std::runtime_error("expected Boolean");
return value;
}

inline FieldInfo* Field(Il2CppObject* object, const char* name, const char* expected)
{
auto* field = object && object->klass ? IL2CppClassHelper(object->klass).GetField(name).get_info() : nullptr;
auto* type = field && field->type ? il2cpp_class_from_type(field->type) : nullptr;
if (!field || !field->type || (field->type->attrs & FIELD_ATTRIBUTE_STATIC) || !type || !type->name
|| std::strcmp(type->name, expected) != 0)
throw std::runtime_error("field contract changed");
return field;
}

inline Il2CppObject* ReferenceField(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);
}

inline bool BooleanField(Il2CppObject* object, const char* name)
{ return object && Boolean(il2cpp_field_get_value_object(Field(object, name, "Boolean"), object)); }

} // namespace Il2CppChecked
11 changes: 8 additions & 3 deletions mods/src/il2cpp/il2cpp_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ class IL2CppFieldHelper
#endif
}

FieldInfo* get_info() const
{
return this->fieldInfo;
}

inline ptrdiff_t offset() const
{
return this->fieldInfo->offset;
Expand Down Expand Up @@ -337,10 +342,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};
}
Expand Down
40 changes: 40 additions & 0 deletions mods/src/il2cpp/runtime.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include "il2cpp_helper.h"
#include <il2cpp-tabledefs.h>

// Mechanics shared by optional runtime features. Signature/overload selection,
// argument storage and feature-specific failure policy remain with the caller.
namespace Il2CppRuntime
{
// 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)
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<bool*>(il2cpp_object_unbox(boxed));
if (!data)
return false;
value = *data;
return true;
}
} // namespace Il2CppRuntime
8 changes: 8 additions & 0 deletions mods/src/patches/instant_warp_policy.h
Original file line number Diff line number Diff line change
@@ -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();
65 changes: 26 additions & 39 deletions mods/src/patches/parts/instant_warp_confirmation.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "config.h"
#include "errormsg.h"
#include "patches/instant_warp_policy.h"
#include "ship_name_match.h"

#include <prime/CourseData.h>
Expand All @@ -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;

Expand All @@ -27,49 +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;
}

const auto display_words = ShipNameMatch::DisplayWords(fleet);
std::string hull_name;
if (fleet != nullptr) {
hull_name = ShipNameMatch::GameDisplayName(fleet);
}

const auto matches = [&display_words](const std::vector<std::string>& names, bool all) -> bool {
if (all) return true;
if (display_words.empty()) return false;
return std::ranges::any_of(names, [&](const auto& configured) {
return ShipNameMatch::MatchesDisplay(display_words, 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<int>(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:
Expand All @@ -78,6 +46,24 @@ void CoursePromptPopupViewController_AboutToShow_Hook(auto original, CoursePromp
}
} // namespace

InstantWarpConfirmation ResolveInstantWarpConfirmation(FleetPlayerData* fleet)
{
const auto& cfg = Config::Get();
const auto display_words = ShipNameMatch::DisplayWords(fleet);
const auto matches = [&display_words](const std::vector<std::string>& names, bool all) {
return all || (!display_words.empty() && std::ranges::any_of(names, [&](const auto& configured) {
return ShipNameMatch::MatchesDisplay(display_words, 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();
Expand Down Expand Up @@ -110,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();
}
16 changes: 6 additions & 10 deletions mods/src/patches/parts/loading_screen_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include "config.h"
#include "errormsg.h"

#include <il2cpp/il2cpp_helper.h>
#include <il2cpp/runtime.h>
#include <il2cpp/il2cpp-functions.h>
#include <spdlog/spdlog.h>

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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<bool*>(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)
Expand Down
Loading
Loading