diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 93faa6b7a..943c169b2 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -471,6 +471,10 @@ quit = "F10" # F9 restart = "F9" +# Re-read this settings file now. It is also re-read by itself about a second after it is saved; changes to +# [patches] still need a restart. +reload_settings = "CTRL-SHIFT-F6" + # CTRL-ALT-MINUS set_hotkeys_disabled = "CTRL-ALT-MINUS" diff --git a/libs/il2cpp/include/il2cpp/hook.h b/libs/il2cpp/include/il2cpp/hook.h new file mode 100644 index 000000000..a264d2bae --- /dev/null +++ b/libs/il2cpp/include/il2cpp/hook.h @@ -0,0 +1,93 @@ +#pragma once + +// Detours on managed methods and runtime functions. A missing method is already reported by its lookup; these +// report a detour SPUD could not install, which SPUD_STATIC_DETOUR alone would drop without a word. Like it, they +// install permanently: nothing removes them, so their stubs skip SPUD's per-call entry counting. +// +// Hooks take the original as their first parameter: +// void ScreenManager_Update_Hook(auto original, ScreenManager* _this) { ...; original(_this); } +// IL2CPP_HOOK(ScreenManager::Class::Method("Update"), ScreenManager_Update_Hook); +// Each expansion installs one detour on its first call, so expand it once per target: a second call from the same +// place returns the first call's trampoline and installs nothing. + +#include "il2cpp.h" + +#include +#include + +#include +#include + +namespace il2cpp::detail +{ +void ReportDetourFailure(std::string_view target, const char* error); + +constexpr const char* DetourErrorName(spud::detour_error error) +{ + using E = spud::detour_error; + switch (error) { + case E::kOk: + return "ok"; + case E::kUnsupportedArch: + return "unsupported architecture"; + case E::kDecodeFailed: + return "prologue could not be decoded"; + case E::kInsufficientBytes: + return "function too short for a detour"; + case E::kProtectionFailed: + return "page protection could not be changed"; + case E::kSuspensionUnavailable: + return "other threads could not be suspended"; + case E::kTargetChanged: + return "target changed while installing"; + case E::kBusy: + return "detour busy"; + case E::kChainFull: + return "too many detours on one function"; + case E::kControlFlowEnforced: + return "control-flow protection"; + case E::kNotFound: + return "not found"; + case E::kUnwindFailed: + return "unwind metadata could not be registered"; + case E::kRollbackFailed: + return "rollback failed"; + } + return "unknown error"; +} + +inline bool Installed(spud::detour_error error, std::string_view target) +{ + if (error == spud::detour_error::kOk) + return true; + const auto detail = spud::last_failure_detail(); + std::string message = DetourErrorName(error); + if (detail.reason) + message += std::string(": ") + detail.reason; + ReportDetourFailure(target, message.c_str()); + return false; +} + +inline bool Installed(spud::detour_error error, const Method& target) +{ return error == spud::detour_error::kOk || Installed(error, target.Name()); } +} // namespace il2cpp::detail + +// Detours `address` with `hook`; `name` (a string or an il2cpp::Method) labels a failure. Evaluates to the +// trampoline that calls the original, or null when nothing was installed. +#define IL2CPP_DETOUR(address, hook, name) \ + ([&](void* target_) { \ + using Trampoline_ = decltype(SPUD_AUTO_HOOK(target_, hook).trampoline()); \ + static const Trampoline_ trampoline_ = [&]() -> Trampoline_ { \ + if (!target_) \ + return nullptr; \ + auto detour_ = SPUD_AUTO_HOOK(target_, hook); \ + if (!::il2cpp::detail::Installed(detour_.try_install_permanent(), name)) \ + return nullptr; \ + return detour_.trampoline(); \ + }(); \ + return trampoline_; \ + }(reinterpret_cast(address))) + +// Detours the native code of a managed method from Class::Method. A missing method installs nothing. +#define IL2CPP_HOOK(method, hook) \ + ([&](const ::il2cpp::Method& method_) { return IL2CPP_DETOUR(method_.Pointer(), hook, method_); }(method)) diff --git a/libs/il2cpp/include/il2cpp/il2cpp.h b/libs/il2cpp/include/il2cpp/il2cpp.h index 13a37c4c2..a2fc9517c 100644 --- a/libs/il2cpp/include/il2cpp/il2cpp.h +++ b/libs/il2cpp/include/il2cpp/il2cpp.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,10 @@ namespace il2cpp // Missing classes and members, and signatures that no longer match the binding, are reported // once per lookup. Without a handler, reports go to stderr. void SetErrorHandler(void (*handler)(std::string_view message)); +// Sends a message through the same handler; for libraries built on this one. +void Report(std::string_view message); +// "Namespace.Outer/Inner", for diagnostics. +std::string ClassName(Il2CppClass* klass); // Compile-time member name, used as a template argument: Call<"Show">(). template struct Name { @@ -51,6 +56,8 @@ class Method // Generated code takes `this` first (for instance methods) and a trailing const MethodInfo*. void* Pointer() const { return info_ ? reinterpret_cast(info_->methodPointer) : nullptr; } + // "Namespace.Class.Method", for diagnostics. + std::string Name() const; private: const MethodInfo* info_ = nullptr; @@ -76,6 +83,9 @@ namespace detail Il2CppClass* StringClass(); Il2CppClass* TypeClass(); + // System.Int32 and friends from corlib, by element type; null for anything else. + Il2CppClass* PrimitiveClass(Il2CppTypeEnum type); + void ReportFault(const char* what); template concept Bound = requires { T::Class::Raw(); }; @@ -119,11 +129,11 @@ namespace detail } template struct Unref { - using Type = T; + using Type = T; static constexpr bool kByRef = false; }; template struct Unref> { - using Type = T; + using Type = T; static constexpr bool kByRef = true; }; @@ -150,13 +160,14 @@ namespace detail } template inline constexpr std::array kParams{SlotOf()...}; - template constexpr Signature SignatureOf() + template constexpr Signature SignatureOf() { return {SlotOf(), kParams}; } - Il2CppClass* FindClass(std::string_view assembly, std::string_view namespaze, std::string_view name); - Method FindMethod(Il2CppClass* klass, std::string_view name, int count); - Method FindMethod(Il2CppClass* klass, std::string_view name, std::initializer_list params); - const MethodInfo* ResolveMethod(Il2CppClass* klass, std::string_view name, const Signature& signature, bool is_static); + Il2CppClass* FindClass(std::string_view assembly, std::string_view namespaze, std::string_view name); + Method FindMethod(Il2CppClass* klass, std::string_view name, int count); + Method FindMethod(Il2CppClass* klass, std::string_view name, std::initializer_list params); + const MethodInfo* ResolveMethod(Il2CppClass* klass, std::string_view name, const Signature& signature, + bool is_static); const MethodInfo* ResolveProperty(Il2CppClass* klass, std::string_view name, const Slot& value, bool setter, bool is_static); FieldInfo* ResolveField(Il2CppClass* klass, std::string_view name, const Slot& value, bool is_static); @@ -184,9 +195,9 @@ namespace detail } } - template using Native = std::conditional_t< - Unref::kByRef, typename Unref::Type*, - std::conditional_t, void*, T>>; + template + using Native = std::conditional_t::kByRef, typename Unref::Type*, + std::conditional_t, void*, T>>; template Native ToNative(T& value) { if constexpr (Unref::kByRef) @@ -357,11 +368,13 @@ template struct Class { static Il2CppClass* const klass = detail::FindClass(Assembly.View(), Namespace.View(), TypeName.View()); return klass; } + // The System.Type for this class. Cached: the runtime keeps type objects in a rooted, append-only cache, and + // looking one up takes its reflection lock. static Il2CppReflectionType* TypeObject() { - auto* klass = Raw(); - return klass ? reinterpret_cast(il2cpp_type_get_object(il2cpp_class_get_type(klass))) - : nullptr; + static Il2CppReflectionType* const type = + Raw() ? reinterpret_cast(il2cpp_type_get_object(il2cpp_class_get_type(Raw()))) : nullptr; + return type; } // Lookups for detours. A count of -1 accepts any arity; parameter names are "Namespace.Type". @@ -370,6 +383,21 @@ template struct Class { static il2cpp::Method Method(std::string_view name, std::initializer_list params) { return detail::FindMethod(Raw(), name, params); } + // An instance method with its complete C++ signature, for hooks that must reject client ABI drift. + template static il2cpp::Method Method(std::string_view name) + { + const auto* method = detail::MethodCall::Resolve(Raw(), name, false); + if (method && (method->has_full_generic_sharing_signature || (method->flags & METHOD_ATTRIBUTE_ABSTRACT))) { + Report("[IL2CPP] cannot hook shared or abstract method " + ClassName(Raw()) + "." + std::string(name)); + return {}; + } + return il2cpp::Method(method); + } + + // Checks an instance field's schema without reading an object, for hooks that write several related fields. + template static bool HasField() + { return detail::ResolveField(Raw(), N.View(), detail::SlotOf(), false) != nullptr; } + template static auto Call(A&&... args) { using Sig = detail::CallOf...>; @@ -401,6 +429,10 @@ template struct Class { // Base for reference-type bindings. A binding pointer is the managed object itself; bindings add // no data members. Receivers must not be null. Missing members return a value-initialized result. struct Object : Il2CppObject { + // Checks a method on the receiver's runtime class, including inflated collection types. + template il2cpp::Method Method(this Self& self, std::string_view name) + { return il2cpp::Method(detail::MethodCall::Resolve(self.klass, name, false)); } + template T Get(this Self& self) { auto* getter = detail::Lookup(self, [](Il2CppClass* klass) { @@ -417,16 +449,14 @@ struct Object : Il2CppObject { } template T Field(this Self& self) { - auto* field = detail::Lookup(self, [](Il2CppClass* klass) { - return detail::ResolveField(klass, N.View(), detail::SlotOf(), false); - }); + auto* field = detail::Lookup( + self, [](Il2CppClass* klass) { return detail::ResolveField(klass, N.View(), detail::SlotOf(), false); }); return detail::ReadField(field, &self); } template void SetField(this Self& self, T value) { - auto* field = detail::Lookup(self, [](Il2CppClass* klass) { - return detail::ResolveField(klass, N.View(), detail::SlotOf(), false); - }); + auto* field = detail::Lookup( + self, [](Il2CppClass* klass) { return detail::ResolveField(klass, N.View(), detail::SlotOf(), false); }); detail::WriteField(field, &self, value); } // Call<"Name", R>(args) selects the overload by the C++ argument types; std::ref marks ref/out @@ -468,6 +498,98 @@ template T* New(A... args) return object; } +// Allocate T without running any constructor, for classes whose parameterless constructor has no generated code. +// Every field the game will read has to be set by the caller. Null when the class is missing. +template T* Allocate() +{ + auto* klass = T::Class::Raw(); + return klass ? static_cast(il2cpp_object_new(klass)) : nullptr; +} + +// System.Object, the element class of object[]. +Il2CppClass* ObjectClass(); + +// The corlib class of a primitive C++ type: System.Byte for uint8_t, System.Int64 for int64_t. +template Il2CppClass* PrimitiveClass() +{ + static_assert(std::is_arithmetic_v, "only primitives have a corlib class here"); + return detail::PrimitiveClass(detail::Primitive()); +} + +// A boxed primitive, for object parameters and object[] arguments. +template Il2CppObject* Box(T value) +{ + auto* klass = PrimitiveClass(); + return klass ? il2cpp_value_box(klass, &value) : nullptr; +} + +// A new managed string. Like any managed object it lives while something managed or the native stack holds it. +inline Il2CppString* NewString(std::string_view utf8) +{ return il2cpp_string_new_len(utf8.data(), static_cast(utf8.size())); } + +// The string as UTF-8; empty for null. +std::string ToUtf8(const Il2CppString* str); + +// Compares a managed string with ASCII text without converting it, for checks on hot paths. +inline bool Equals(const Il2CppString* str, std::string_view ascii) +{ + if (!str || static_cast(str->length) != ascii.size()) + return false; + for (size_t i = 0; i < ascii.size(); ++i) + if (str->chars[i] != static_cast(ascii[i])) + return false; + return true; +} + +// The text of any object: a string as itself, anything else through its ToString override. +inline Il2CppString* ToString(Object* object) +{ + if (!object) + return nullptr; + if (object->klass == detail::StringClass()) + return reinterpret_cast(object); + return object->Call<"ToString", Il2CppString*>(); +} + +// Instance fields of the object's class and its bases, most derived first. For reading game data whose field +// names drift between builds; bindings should name their fields. +template void ForEachInstanceField(Il2CppObject* object, Visit visit) +{ + for (auto* klass = object ? object->klass : nullptr; klass; klass = il2cpp_class_get_parent(klass)) { + void* iter = nullptr; + while (auto* field = il2cpp_class_get_fields(klass, &iter)) + if (!(field->type->attrs & FIELD_ATTRIBUTE_STATIC)) + visit(field); + } +} + +// A field found by ForEachInstanceField. T must match the field's layout; nothing checks it here. +template T ReadField(Il2CppObject* object, FieldInfo* field) +{ return detail::ReadField(field, object); } + +// Runs the method a delegate is bound to on its target directly, skipping the delegate's invoke stub. Unlike +// Call, a managed exception is not caught here, so the caller can fall back: catch Il2CppExceptionWrapper. +template R InvokeBound(Il2CppDelegate* delegate, A... args) +{ return detail::InvokeNative(delegate->method, delegate->target, args...); } + +// Runs `read` and, on Windows, survives an access violation inside it: reading game objects whose layout drifted +// between client builds should cost a log line, not the game. False, after reporting `what`, when one was caught. +template bool Guarded(const char* what, Fn&& read) +{ +#if _WIN32 + __try { + read(); + } __except (1 /* EXCEPTION_EXECUTE_HANDLER */) { + detail::ReportFault(what); + return false; + } +#else + (void)what; + read(); +#endif + return true; +} + // Managed T[]. Element references are borrowed; use Set to store references with a GC barrier. // Set supports references and primitives only. template struct Array : Il2CppObject { @@ -509,6 +631,8 @@ template struct List : Object { { Call<"Add">(value); } void RemoveAt(int32_t index) { Call<"RemoveAt">(index); } + void Clear() + { Call<"Clear">(); } struct Iterator { List* list; @@ -608,7 +732,43 @@ class GcRoots bool healthy_ = true; }; -// Unity engine internal calls, e.g. "UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)". -template Signature* ResolveICall(const char* name) -{ return reinterpret_cast(il2cpp_resolve_icall(name)); } +namespace detail +{ + void ReportMissingICall(const char* name); +} // namespace detail + +// A Unity engine internal call. One that a Unity upgrade renamed is reported once, when resolved, and then returns +// the default value instead of calling through null; test it with operator bool to choose another default. +template class ICall; +template class ICall +{ +public: + explicit ICall(const char* name) + : fn_(reinterpret_cast(il2cpp_resolve_icall(name))) + { + if (!fn_) + detail::ReportMissingICall(name); + } + + explicit operator bool() const + { return fn_ != nullptr; } + + R operator()(A... args) const + { + if constexpr (std::is_void_v) { + if (fn_) + fn_(args...); + } else { + return fn_ ? fn_(args...) : R{}; + } + } + +private: + R (*fn_)(A...); +}; + +// Resolves e.g. "UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)". Keep the result in a static: resolving is a +// lookup by name. +template ICall ResolveICall(const char* name) +{ return ICall(name); } } // namespace il2cpp diff --git a/libs/il2cpp/include/il2cpp/method_contract.h b/libs/il2cpp/include/il2cpp/method_contract.h new file mode 100644 index 000000000..ca9004941 --- /dev/null +++ b/libs/il2cpp/include/il2cpp/method_contract.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +namespace il2cpp::contract +{ +inline bool Type(const Il2CppType* type, const char* name) +{ + if (!type || type->byref) + return false; + auto* actual = il2cpp_type_get_name(type); + const bool matches = actual && std::strcmp(actual, name) == 0; + il2cpp_free(actual); + return matches; +} + +// Resolve the entire managed signature, including static/instance dispatch. +// A renamed, ambiguous or generic method is not a compatible callback. +inline const MethodInfo* Resolve(Il2CppClass* cls, const char* name, bool is_static, const char* result, + std::initializer_list parameters) +{ + if (!cls) + return nullptr; + const MethodInfo* found = nullptr; + void* iterator = nullptr; + while (auto* method = il2cpp_class_get_methods(cls, &iterator)) { + if (std::strcmp(method->name, name) != 0 || !method->methodPointer || method->is_generic || method->is_inflated + || bool(method->flags & METHOD_ATTRIBUTE_STATIC) != is_static || method->parameters_count != parameters.size() + || !Type(method->return_type, result)) + continue; + bool matches = true; + unsigned i = 0; + for (auto* parameter : parameters) + matches = Type(method->parameters[i++], parameter) && matches; + if (!matches) + continue; + if (found) + return nullptr; + found = method; + } + return found; +} + +inline void* Pointer(const MethodInfo* method) +{ return method ? reinterpret_cast(method->methodPointer) : nullptr; } +} // namespace il2cpp::contract diff --git a/libs/il2cpp/src/il2cpp.cc b/libs/il2cpp/src/il2cpp.cc index 334a8a519..c859395ef 100644 --- a/libs/il2cpp/src/il2cpp.cc +++ b/libs/il2cpp/src/il2cpp.cc @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -10,28 +11,20 @@ namespace il2cpp { +std::string ClassName(Il2CppClass* klass) +{ + if (!klass) + return ""; + if (auto* outer = il2cpp_class_get_declaring_type(klass)) + return ClassName(outer) + "/" + il2cpp_class_get_name(klass); + std::string name = il2cpp_class_get_namespace(klass); + return name.empty() ? il2cpp_class_get_name(klass) : name + "." + il2cpp_class_get_name(klass); +} + namespace { std::atomic error_handler = nullptr; - void Report(const std::string& message) - { - if (auto* handler = error_handler.load()) - handler(message); - else - std::fprintf(stderr, "%s\n", message.c_str()); - } - - std::string ClassName(Il2CppClass* klass) - { - if (!klass) - return ""; - if (auto* outer = il2cpp_class_get_declaring_type(klass)) - return ClassName(outer) + "/" + il2cpp_class_get_name(klass); - std::string name = il2cpp_class_get_namespace(klass); - return name.empty() ? il2cpp_class_get_name(klass) : name + "." + il2cpp_class_get_name(klass); - } - std::string TypeName(const Il2CppType* type) { auto* raw = type ? il2cpp_type_get_name(type) : nullptr; @@ -158,8 +151,61 @@ namespace void SetErrorHandler(void (*handler)(std::string_view message)) { error_handler = handler; } +void Report(std::string_view message) +{ + if (auto* handler = error_handler.load()) + handler(message); + else + std::fprintf(stderr, "%.*s\n", static_cast(message.size()), message.data()); +} + +std::string Method::Name() const +{ return info_ ? ClassName(info_->klass) + "." + info_->name : ""; } + +Il2CppClass* ObjectClass() +{ + static auto* klass = il2cpp_class_from_name(il2cpp_get_corlib(), "System", "Object"); + return klass; +} + +std::string ToUtf8(const Il2CppString* str) +{ + std::string result; + if (!str) + return result; + result.reserve(static_cast(str->length)); + const auto* chars = str->chars; + for (int32_t i = 0; i < str->length; ++i) { + uint32_t c = chars[i]; + // A surrogate pair is one code point; a lone surrogate becomes U+FFFD. + if (c >= 0xD800 && c <= 0xDBFF && i + 1 < str->length && chars[i + 1] >= 0xDC00 && chars[i + 1] <= 0xDFFF) + c = 0x10000 + ((c - 0xD800) << 10) + (chars[++i] - 0xDC00); + else if (c >= 0xD800 && c <= 0xDFFF) + c = 0xFFFD; + if (c < 0x80) { + result += static_cast(c); + } else if (c < 0x800) { + result += static_cast(0xC0 | (c >> 6)); + result += static_cast(0x80 | (c & 0x3F)); + } else if (c < 0x10000) { + result += static_cast(0xE0 | (c >> 12)); + result += static_cast(0x80 | ((c >> 6) & 0x3F)); + result += static_cast(0x80 | (c & 0x3F)); + } else { + result += static_cast(0xF0 | (c >> 18)); + result += static_cast(0x80 | ((c >> 12) & 0x3F)); + result += static_cast(0x80 | ((c >> 6) & 0x3F)); + result += static_cast(0x80 | (c & 0x3F)); + } + } + return result; +} + namespace detail { + void ReportMissingICall(const char* name) + { Report(std::string("[IL2CPP] missing internal call ") + name); } + Il2CppClass* StringClass() { static auto* klass = il2cpp_class_from_name(il2cpp_get_corlib(), "System", "String"); @@ -172,6 +218,34 @@ namespace detail return klass; } + Il2CppClass* PrimitiveClass(Il2CppTypeEnum type) + { + static constexpr std::pair kNames[] = { + {IL2CPP_TYPE_BOOLEAN, "Boolean"}, {IL2CPP_TYPE_CHAR, "Char"}, {IL2CPP_TYPE_I1, "SByte"}, + {IL2CPP_TYPE_U1, "Byte"}, {IL2CPP_TYPE_I2, "Int16"}, {IL2CPP_TYPE_U2, "UInt16"}, + {IL2CPP_TYPE_I4, "Int32"}, {IL2CPP_TYPE_U4, "UInt32"}, {IL2CPP_TYPE_I8, "Int64"}, + {IL2CPP_TYPE_U8, "UInt64"}, {IL2CPP_TYPE_R4, "Single"}, {IL2CPP_TYPE_R8, "Double"}, + }; + static std::array, std::size(kNames)> classes{}; + for (size_t i = 0; i < std::size(kNames); ++i) { + if (kNames[i].first != type) + continue; + auto* klass = classes[i].load(); + if (!klass) { + klass = il2cpp_class_from_name(il2cpp_get_corlib(), "System", kNames[i].second); + classes[i].store(klass); + } + return klass; + } + return nullptr; + } + + void ReportFault(const char* what) + { Report(std::string("[IL2CPP] access violation while reading ") + what); } + + void ReportDetourFailure(std::string_view target, const char* error) + { Report("[IL2CPP] could not detour " + std::string(target) + ": " + error); } + Il2CppClass* FindClass(std::string_view assembly, std::string_view namespaze, std::string_view name) { const std::string assembly_name(assembly), namespace_name(namespaze); diff --git a/libs/unity/include/unity/core.h b/libs/unity/include/unity/core.h new file mode 100644 index 000000000..a347b04cc --- /dev/null +++ b/libs/unity/include/unity/core.h @@ -0,0 +1,349 @@ +#pragma once + +// UnityEngine's core types for IL2CPP bindings: math structs, objects and their lifetime, scene hierarchy, camera, +// screen and time, textures and sprites. Nothing here knows about any particular game. + +#include + +#include +#include +#include +#include + +namespace unity +{ +struct Vector2 { + float x, y; +}; + +struct Vector3 { + float x, y, z; +}; + +struct Quaternion { + float x, y, z, w; +}; + +struct Color { + float r, g, b, a; +}; + +struct Rect { + float x, y, width, height; +}; + +// System.TimeSpan: 100 ns ticks. +struct TimeSpan { + int64_t ticks; +}; + +using ObjectClass = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Object">; + +// Unity's bool(Object): false for null, and for objects whose native side was destroyed even while a GC handle +// still holds the managed wrapper. +inline bool Alive(Il2CppObject* object) +{ return object && ObjectClass::Call<"op_Implicit", bool>(object); } + +inline void Destroy(Il2CppObject* object) +{ ObjectClass::Call<"Destroy">(object); } + +// The first loaded object of T. Scans every loaded object; keep it off per-frame paths. +template T* FindObject() +{ + auto* type = T::Class::TypeObject(); + return type ? static_cast(ObjectClass::Call<"FindObjectOfType", Il2CppObject*>(type)) : nullptr; +} + +// Every active loaded T. Scans every loaded object; keep it off per-frame paths. +template il2cpp::Array* FindObjectsOfType() +{ + auto* type = T::Class::TypeObject(); + return type ? ObjectClass::Call<"FindObjectsOfType", il2cpp::Array*>(type) : nullptr; +} + +// Every loaded T, active or not, including prefabs and other assets. Scans every loaded object; keep it off +// per-frame paths. +template il2cpp::Array* FindObjectsOfTypeAll() +{ + using Resources = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Resources">; + auto* type = T::Class::TypeObject(); + return type ? Resources::Call<"FindObjectsOfTypeAll", il2cpp::Array*>(type) : nullptr; +} + +// Behaviour.isActiveAndEnabled through its internal call, for bindings whose managed property may have drifted: +// without the call everything counts as active. +inline bool ActiveAndEnabled(il2cpp::Object* behaviour) +{ + static const il2cpp::ICall call{"UnityEngine.Behaviour::get_isActiveAndEnabled()"}; + return !call || call(behaviour); +} + +struct Transform; + +struct GameObject : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "GameObject">; + + static GameObject* Find(std::string_view path) + { return Class::Call<"Find", GameObject*>(il2cpp::NewString(path)); } + + template il2cpp::Array* GetComponentsInChildren(bool include_inactive) + { + auto* type = T::Class::TypeObject(); + return type ? Call<"GetComponentsInChildren", il2cpp::Array*>(type, include_inactive) : nullptr; + } + + // A new, active GameObject with only a Transform, at the scene root. + static GameObject* Create(std::string_view name) + { return il2cpp::New(il2cpp::NewString(name)); } + + std::string Name() + { return il2cpp::ToUtf8(Get<"name", Il2CppString*>()); } + bool NameIs(std::string_view ascii) + { return il2cpp::Equals(Get<"name", Il2CppString*>(), ascii); } + Transform* transform() + { return Get<"transform", Transform*>(); } + void SetActive(bool active) + { Call<"SetActive">(active); } + + // Passing System.Type selects the (Type) overloads over the string and generic ones. A null Type would throw in + // managed code, so a missing class yields null instead. + template T* GetComponent() + { + auto* type = T::Class::TypeObject(); + return type ? Call<"GetComponent", T*>(type) : nullptr; + } + // The first active T on this GameObject or below it, depth first. Searches one hierarchy, where FindObject + // searches every loaded object. This Unity only has the (Type, bool includeInactive) overload. + template T* GetComponentInChildren() + { + auto* type = T::Class::TypeObject(); + return type ? Call<"GetComponentInChildren", T*>(type, false) : nullptr; + } + template T* AddComponent() + { + auto* type = T::Class::TypeObject(); + return type ? Call<"AddComponent", T*>(type) : nullptr; + } +}; + +struct Transform : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Transform">; + + int32_t childCount() + { return Get<"childCount", int32_t>(); } + Transform* GetChild(int32_t index) + { return Call<"GetChild", Transform*>(index); } + Transform* parent() + { return Get<"parent", Transform*>(); } + // The topmost transform of this hierarchy; itself when it has no parent. + Transform* root() + { return Get<"root", Transform*>(); } + GameObject* gameObject() + { return Get<"gameObject", GameObject*>(); } + Vector3 localScale() + { return Get<"localScale", Vector3>(); } + void SetLocalScale(Vector3 value) + { Set<"localScale">(value); } + void SetLocalEulerAngles(Vector3 value) + { Set<"localEulerAngles">(value); } + void SetParent(Transform* parent, bool worldPositionStays) + { Call<"SetParent">(parent, worldPositionStays); } + void SetAsFirstSibling() + { Call<"SetAsFirstSibling">(); } + // A descendant by slash-separated path below this one, e.g. "Hint/Background". + Transform* Find(std::string_view path) + { return Call<"Find", Transform*>(il2cpp::NewString(path)); } + + // This transform or the first descendant, depth first, whose GameObject is called `name`. + Transform* FindNamed(std::string_view name) + { + if (auto* object = gameObject(); object && object->NameIs(name)) + return this; + for (int32_t i = 0; i < childCount(); ++i) + if (auto* child = GetChild(i); auto* found = child ? child->FindNamed(name) : nullptr) + return found; + return nullptr; + } + + // The direct child whose GameObject is called `name`. + Transform* ChildNamed(std::string_view name) + { + for (int32_t i = 0; i < childCount(); ++i) + if (auto* child = GetChild(i); child && child->gameObject() && child->gameObject()->NameIs(name)) + return child; + return nullptr; + } +}; + +struct RectTransform : Transform { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "RectTransform">; + + Vector2 anchorMin() + { return Get<"anchorMin", Vector2>(); } + Vector2 anchorMax() + { return Get<"anchorMax", Vector2>(); } + Vector2 offsetMin() + { return Get<"offsetMin", Vector2>(); } + Vector2 offsetMax() + { return Get<"offsetMax", Vector2>(); } + void SetAnchorMin(Vector2 value) + { Set<"anchorMin">(value); } + void SetAnchorMax(Vector2 value) + { Set<"anchorMax">(value); } + void SetPivot(Vector2 value) + { Set<"pivot">(value); } + void SetOffsetMin(Vector2 value) + { Set<"offsetMin">(value); } + void SetOffsetMax(Vector2 value) + { Set<"offsetMax">(value); } + + void Place(Vector2 anchor_min, Vector2 anchor_max, Vector2 pivot, Vector2 size, Vector2 position) + { + Set<"anchorMin">(anchor_min); + Set<"anchorMax">(anchor_max); + Set<"pivot">(pivot); + Set<"sizeDelta">(size); + Set<"anchoredPosition">(position); + } +}; + +// UnityEngine.Component, the base of every MonoBehaviour. Use As() on other bindings. +struct Component : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Component">; + + Transform* transform() + { return Get<"transform", Transform*>(); } + GameObject* gameObject() + { return Get<"gameObject", GameObject*>(); } +}; + +// A copy of `original` (a component copies its whole GameObject) under `parent`. Uses the non-generic +// Instantiate(Object, Transform, bool): some clients expose the two-argument overload only as Instantiate, which +// would need generic inflation. +template T* Instantiate(T* original, Transform* parent, bool world_position_stays = false) +{ + return static_cast(ObjectClass::Call<"Instantiate", il2cpp::Object*(il2cpp::Object*, Transform*, bool)>( + original, parent, world_position_stays)); +} + +struct Behaviour : Component { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Behaviour">; + + void SetEnabled(bool value) + { Set<"enabled">(value); } + bool isActiveAndEnabled() + { return Get<"isActiveAndEnabled", bool>(); } +}; + +struct Camera : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Camera">; + + enum class ClearFlags : int32_t { + Skybox = 1, + SolidColor = 2, + Depth = 3, + Nothing = 4, + }; + + void SetFarClipPlane(float value) + { Set<"farClipPlane">(value); } + void SetClearFlags(ClearFlags value) + { Set<"clearFlags">(value); } + void SetBackgroundColor(Color value) + { Set<"backgroundColor">(value); } +}; + +struct AnimationCurve : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "AnimationCurve">; + float Evaluate(float value) + { return Call<"Evaluate", float>(value); } +}; + +struct Shader : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Shader">; + std::string Name() + { return il2cpp::ToUtf8(Get<"name", Il2CppString*>()); } +}; + +struct Material : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Material">; + std::string Name() + { return il2cpp::ToUtf8(Get<"name", Il2CppString*>()); } + Shader* shader() + { return Get<"shader", Shader*>(); } + bool HasProperty(Il2CppString* name) + { return Call<"HasProperty", bool>(name); } + float GetFloat(Il2CppString* name) + { return Call<"GetFloat", float>(name); } + void SetFloat(Il2CppString* name, float value) + { Call<"SetFloat">(name, value); } +}; + +struct Renderer : Component { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Renderer">; + il2cpp::Array* SharedMaterials() + { return Get<"sharedMaterials", il2cpp::Array*>(); } +}; + +struct Canvas : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.UIModule", "UnityEngine", "Canvas">; +}; + +struct Screen { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Screen">; + + static int32_t width() + { return Class::Get<"width", int32_t>(); } + static int32_t height() + { return Class::Get<"height", int32_t>(); } +}; + +using Cursor = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Cursor">; + +// UnityEngine.Time through its internal calls, which skip the managed wrappers. +struct Time { + // -1 when Unity lacks the call. + static int32_t frameCount() + { + static const il2cpp::ICall call{"UnityEngine.Time::get_frameCount()"}; + return call ? call() : -1; + } + static float deltaTime() + { + static const il2cpp::ICall call{"UnityEngine.Time::get_deltaTime()"}; + return call(); + } +}; + +struct Texture2D : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Texture2D">; + + int32_t width() + { return Get<"width", int32_t>(); } + int32_t height() + { return Get<"height", int32_t>(); } + + // A texture decoded from PNG or JPG bytes, or null. + static Texture2D* Decode(const uint8_t* data, size_t size) + { + using ImageConversion = il2cpp::Class<"UnityEngine.ImageConversionModule", "UnityEngine", "ImageConversion">; + auto* bytes = data && size ? il2cpp::Array::New(il2cpp::PrimitiveClass(), size) : nullptr; + if (!bytes) + return nullptr; + std::memcpy(bytes->data(), data, size); + // Texture2D(width, height, TextureFormat.RGBA32, mipChain); LoadImage resizes it. + auto* texture = il2cpp::New(int32_t{2}, int32_t{2}, int32_t{4}, false); + return texture && ImageConversion::Call<"LoadImage", bool>(texture, bytes, false) ? texture : nullptr; + } +}; + +struct Sprite : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.CoreModule", "UnityEngine", "Sprite">; + + static Sprite* Create(Texture2D* texture, Rect rect, Vector2 pivot) + { return Class::Call<"Create", Sprite*>(texture, rect, pivot); } + + float pixelsPerUnit() + { return Get<"pixelsPerUnit", float>(); } +}; +} // namespace unity diff --git a/mods/src/prime/KeyCode.h b/libs/unity/include/unity/input.h similarity index 78% rename from mods/src/prime/KeyCode.h rename to libs/unity/include/unity/input.h index 6f4c79fc2..15891a816 100644 --- a/mods/src/prime/KeyCode.h +++ b/libs/unity/include/unity/input.h @@ -1,5 +1,17 @@ #pragma once +// Keyboard and pointer input: Unity's KeyCode, the legacy Input API through its internal calls, a per-frame key +// snapshot, text-field focus, and the Input System types bindings pass around. + +#include "core.h" + +#include + +#include + +namespace unity +{ +// UnityEngine.KeyCode. enum class KeyCode { None = 0, Backspace = 8, @@ -328,6 +340,102 @@ enum class KeyCode { Joystick8Button18 = 508, // 0x000001FC Joystick8Button19 = 509, // 0x000001FD - // Autoamtic maximum + // One past the largest code, for tables indexed by key. Max -}; \ No newline at end of file +}; + +// UnityEngine.Input through its internal calls, which skip the managed wrappers. A call that a Unity upgrade +// renamed is reported once and then reads as nothing pressed. +struct Input { + // True when Unity lacks the call, so a caller that skips key checks on idle frames still checks. + static bool anyKey() + { + static const il2cpp::ICall call{"UnityEngine.Input::get_anyKey()"}; + return !call || call(); + } + static bool anyKeyDown() + { + static const il2cpp::ICall call{"UnityEngine.Input::get_anyKeyDown()"}; + return !call || call(); + } + static bool GetKey(KeyCode key) + { + static const il2cpp::ICall call{"UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)"}; + return call(key); + } + static bool GetKeyDown(KeyCode key) + { + static const il2cpp::ICall call{"UnityEngine.Input::GetKeyDownInt(UnityEngine.KeyCode)"}; + return call(key); + } + static bool GetMouseButton(int32_t button) + { + static const il2cpp::ICall call{"UnityEngine.Input::GetMouseButton(System.Int32)"}; + return call(button); + } + static int32_t touchCount() + { + static const il2cpp::ICall call{"UnityEngine.Input::get_touchCount()"}; + return call(); + } + static Vector3 mousePosition() + { + static const il2cpp::ICall call{ + "UnityEngine.Input::get_mousePosition_Injected(UnityEngine.Vector3&)"}; + Vector3 position{}; + call(&position); + return position; + } +}; + +enum class TouchPhase : int32_t { + Began = 0, + Moved = 1, + Stationary = 2, + Ended = 3, + Canceled = 4, +}; + +struct InputAction : il2cpp::Object { + using Class = il2cpp::Class<"Unity.InputSystem", "UnityEngine.InputSystem", "InputAction">; + + Il2CppString* name() + { return Get<"name", Il2CppString*>(); } +}; + +struct InputActionReference : il2cpp::Object { + using Class = il2cpp::Class<"Unity.InputSystem", "UnityEngine.InputSystem", "InputActionReference">; + + InputAction* action() + { return Get<"action", InputAction*>(); } +}; + +// UnityEngine.InputSystem.InputAction.CallbackContext, passed empty. Generated code takes it by value with the +// platform's C++ ABI, so this layout must match the managed struct. +struct InputActionCallbackContext { + void* state; + int32_t action_index; +}; +static_assert(sizeof(InputActionCallbackContext) == 16); + +// This frame's keyboard. Unity answers each key from its own per-frame state; the snapshot only skips asking on +// idle frames and caches the derived checks. +namespace Key +{ + // Starts a frame's reads. Every hook that reads keys calls it first, since hooks run in no fixed order within a + // frame and reads before it see the previous frame. Cheap after the first call in a frame. + void Poll(); + + // Held during this frame. + bool Pressed(KeyCode key); + // Went down this frame. + bool Down(KeyCode key); + // Any of Ctrl, Alt, AltGr, Shift, Cmd, or Win is held. + bool IsModified(); + + // A TextMeshPro input field has keyboard focus, so typing must not trigger shortcuts. + bool IsInputFocused(); + // Takes keyboard focus away from the focused input field. Unity UI only does this on a click elsewhere. + void ClearInputFocus(); +} // namespace Key +} // namespace unity diff --git a/libs/unity/include/unity/tracker.h b/libs/unity/include/unity/tracker.h new file mode 100644 index 000000000..5305d429a --- /dev/null +++ b/libs/unity/include/unity/tracker.h @@ -0,0 +1,63 @@ +#pragma once + +// Live instances of chosen MonoBehaviour classes, found without asking Unity. Track() detours T's constructor +// and OnDestroy; a GC finalizer and Unity's liveness pass drop instances the game has released without destroying +// them. Instances are borrowed: use them on the game thread, within the frame. + +#include +#include + +#include + +namespace unity +{ +namespace detail +{ + void Constructed(Il2CppObject* object); + void Destroyed(Il2CppObject* object); + // True the first time a native method is seen: constructors and OnDestroy often resolve to one shared base + // implementation, which must be detoured only once. + bool FirstDetour(void* native); + void StartTracking(); + Il2CppObject* Newest(Il2CppClass* klass); + std::vector Instances(Il2CppClass* klass); + + inline void Ctor_Hook(auto original, Il2CppObject* _this, const MethodInfo* method) + { + original(_this, method); + Constructed(_this); + } + + inline void OnDestroy_Hook(auto original, Il2CppObject* _this, const MethodInfo* method) + { + Destroyed(_this); + original(_this, method); + } +} // namespace detail + +// Starts tracking T and its subclasses. Call while installing hooks, before T's instances are created: instances +// that already exist are not found. Alternate parameterless lifecycle methods support pooled components. +template void Track(std::string_view created = ".ctor", std::string_view released = "OnDestroy") +{ + detail::StartTracking(); + // Both lifecycle methods are parameterless; Unity's constructor and OnDestroy are the defaults. + if (auto ctor = T::Class::Method(created, 0); ctor && detail::FirstDetour(ctor.Pointer())) + IL2CPP_HOOK(ctor, detail::Ctor_Hook); + if (auto on_destroy = T::Class::Method(released, 0); on_destroy && detail::FirstDetour(on_destroy.Pointer())) + IL2CPP_HOOK(on_destroy, detail::OnDestroy_Hook); +} + +// The newest live instance of T, or null. Only finds classes passed to Track. +template T* Newest() +{ return static_cast(detail::Newest(T::Class::Raw())); } + +// Every live instance of T, oldest first. A copy, so the caller may call into the game while the GC's finalizer +// thread keeps updating the tracker. +template std::vector Instances() +{ + std::vector result; + for (auto* object : detail::Instances(T::Class::Raw())) + result.push_back(static_cast(object)); + return result; +} +} // namespace unity diff --git a/libs/unity/include/unity/ui.h b/libs/unity/include/unity/ui.h new file mode 100644 index 000000000..b56b0c4a2 --- /dev/null +++ b/libs/unity/include/unity/ui.h @@ -0,0 +1,217 @@ +#pragma once + +// Unity UI and TextMeshPro: canvases, images, buttons, toggles, text, input fields and the event system, plus the +// small helpers for building UI from native code and keeping a decoded image alive across scene reloads. + +#include "core.h" + +#include + +#include +#include + +namespace unity +{ +struct CanvasScaler : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.UI", "CanvasScaler">; + + // The property, which the scaler applies on its next Update; not the managed SetScaleFactor method, which writes + // Canvas.scaleFactor at once. + void SetScaleFactor(float value) + { Set<"scaleFactor">(value); } +}; + +// UnityEngine.UI.Image; color and raycastTarget come from Graphic. +struct Image : Component { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.UI", "Image">; + + enum class Type : int32_t { + Simple = 0, + Sliced = 1, + Tiled = 2, + Filled = 3, + }; + + Sprite* sprite() + { return Get<"sprite", Sprite*>(); } + void SetSprite(Sprite* value) + { Set<"sprite">(value); } + Sprite* overrideSprite() + { return Get<"overrideSprite", Sprite*>(); } + void SetOverrideSprite(Sprite* value) + { Set<"overrideSprite">(value); } + void SetType(Type value) + { Set<"type">(value); } + void SetPreserveAspect(bool value) + { Set<"preserveAspect">(value); } + void SetPixelsPerUnitMultiplier(float value) + { Set<"pixelsPerUnitMultiplier">(value); } + void SetColor(Color value) + { Set<"color">(value); } + void SetRaycastTarget(bool value) + { Set<"raycastTarget">(value); } + void SetVerticesDirty() + { Call<"SetVerticesDirty">(); } +}; + +struct Button : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.UI", "Button">; + + void Press() + { Call<"Press">(); } +}; + +struct EventSystem : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.EventSystems", "EventSystem">; + + static EventSystem* current() + { return Class::Get<"current", EventSystem*>(); } + + GameObject* currentSelectedGameObject() + { return Get<"currentSelectedGameObject", GameObject*>(); } + void SetSelectedGameObject(GameObject* object) + { Call<"SetSelectedGameObject">(object); } +}; + +struct PointerEventData : il2cpp::Object { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.EventSystems", "PointerEventData">; +}; + +struct Toggle : Component { + using Class = il2cpp::Class<"UnityEngine.UI", "UnityEngine.UI", "Toggle">; + + bool isOn() + { return Get<"isOn", bool>(); } + + // Goes through the pointer handler instead of the isOn setter, so interactable checks and listeners behave as for + // a real click. + void SimulateClick() + { + if (auto* event = il2cpp::New(EventSystem::current())) + Call<"OnPointerClick">(event); + } +}; + +struct TMP_Text : Component { + using Class = il2cpp::Class<"Unity.TextMeshPro", "TMPro", "TMP_Text">; + + // TMPro.TextAlignmentOptions, the ones in use. + enum class Alignment : int32_t { + Left = 0x201, + Center = 0x202, + }; + // TMPro.TextOverflowModes, the ones in use. + enum class Overflow : int32_t { + Ellipsis = 1, + }; + + // TMP_FontAsset + il2cpp::Object* font() + { return Get<"font", il2cpp::Object*>(); } + void SetFont(il2cpp::Object* value) + { Set<"font">(value); } + void SetFontSize(float value) + { Set<"fontSize">(value); } + void SetAlignment(Alignment value) + { Set<"alignment">(value); } + void SetColor(Color value) + { Set<"color">(value); } + void SetRaycastTarget(bool value) + { Set<"raycastTarget">(value); } + void SetEnableWordWrapping(bool value) + { Set<"enableWordWrapping">(value); } + void SetRichText(bool value) + { Set<"richText">(value); } + void SetOverflowMode(Overflow value) + { Set<"overflowMode">(value); } + void SetEnableAutoSizing(bool value) + { Set<"enableAutoSizing">(value); } + void SetText(std::string_view value) + { Set<"text">(il2cpp::NewString(value)); } +}; + +struct TextMeshProUGUI : TMP_Text { + using Class = il2cpp::Class<"Unity.TextMeshPro", "TMPro", "TextMeshProUGUI">; +}; + +struct TMP_InputField : il2cpp::Object { + using Class = il2cpp::Class<"Unity.TextMeshPro", "TMPro", "TMP_InputField">; + + bool isFocused() + { return Get<"isFocused", bool>(); } + void ActivateInputField() + { Call<"ActivateInputField">(); } +}; + +// The nearest ancestor of `transform` (or itself) with a Canvas, so an overlay attached there covers the screen. +// `transform` when there is none. +inline Transform* CanvasRoot(Transform* transform) +{ + for (auto* current = transform; current; current = current->parent()) + if (auto* object = current->gameObject(); object && object->GetComponent()) + return current; + return transform; +} + +// An inactive GameObject with a RectTransform under `parent`, for building UI. `root` keeps it alive while the +// caller builds it; once parented, the hierarchy owns it. +inline RectTransform* CreateUiObject(std::string_view name, Transform* parent, il2cpp::GcHandle& root) +{ + auto* object = GameObject::Create(name); + root = il2cpp::GcHandle::Strong(object); + if (!object) + return nullptr; + object->SetActive(false); + auto* transform = object->AddComponent(); + if (transform) + transform->SetParent(parent, false); + return transform; +} + +// An image decoded once and kept while Unity keeps it: a scene reload unloads the texture, and the next use +// decodes it again. Holds strong GC handles, so an instance must not outlive the runtime. +class ImageAsset +{ +public: + // Decodes `data` unless the texture is still loaded. False when it cannot be decoded. + bool Load(const uint8_t* data, size_t size) + { + if (Texture()) + return true; + sprite_.Reset(); + texture_ = il2cpp::GcHandle::Strong(Texture2D::Decode(data, size)); + return Texture() != nullptr; + } + + Texture2D* Texture() const + { + auto* texture = texture_.Get(); + return Alive(texture) ? texture : nullptr; + } + + // A sprite over the whole texture, created on first use. + unity::Sprite* Sprite() + { + if (auto* sprite = sprite_.Get(); Alive(sprite)) + return sprite; + auto* texture = Texture(); + if (!texture) + return nullptr; + const Rect rect{0.0f, 0.0f, static_cast(texture->width()), static_cast(texture->height())}; + auto* sprite = unity::Sprite::Create(texture, rect, Vector2{0.5f, 0.5f}); + sprite_ = il2cpp::GcHandle::Strong(sprite); + return sprite; + } + + // Height over width; 1 without a texture. + float AspectRatio() const + { + auto* texture = Texture(); + return texture && texture->width() > 0 ? static_cast(texture->height()) / texture->width() : 1.0f; + } + +private: + il2cpp::GcHandle texture_; + il2cpp::GcHandle sprite_; +}; +} // namespace unity diff --git a/libs/unity/src/input.cc b/libs/unity/src/input.cc new file mode 100644 index 000000000..6d0d8fab7 --- /dev/null +++ b/libs/unity/src/input.cc @@ -0,0 +1,73 @@ +// Owns the per-frame keyboard snapshot and text-field focus. + +#include +#include + +#include +#include + +namespace unity +{ +namespace +{ + // On an idle frame anyKey is false and no key is asked about at all; otherwise each key is one internal call. + struct Frame { + int number = -1; + bool any_held = false; + bool any_down = false; + int8_t focused = 0; // 0 = not read yet, 1 = true, -1 = false + int8_t modified = 0; + } frame; + + TMP_InputField* FocusedInputField() + { + auto* event_system = EventSystem::current(); + auto* selected = event_system ? event_system->currentSelectedGameObject() : nullptr; + auto* field = selected ? selected->GetComponent() : nullptr; + return field && field->isFocused() ? field : nullptr; + } +} // namespace + +namespace Key +{ + void Poll() + { + // Without the frame count nothing is ever read: no key counts as pressed. + const int number = Time::frameCount(); + if (number < 0 || number == frame.number) + return; + frame = {.number = number, .any_held = Input::anyKey(), .any_down = Input::anyKeyDown()}; + } + + bool Pressed(KeyCode key) + { return frame.any_held && Input::GetKey(key); } + + bool Down(KeyCode key) + { return frame.any_down && Input::GetKeyDown(key); } + + bool IsModified() + { + if (frame.modified == 0) { + constexpr std::array kModifiers{KeyCode::LeftAlt, KeyCode::LeftControl, KeyCode::LeftShift, + KeyCode::RightAlt, KeyCode::RightControl, KeyCode::RightShift, + KeyCode::AltGr, KeyCode::LeftCommand, KeyCode::LeftWindows, + KeyCode::RightCommand, KeyCode::RightWindows}; + frame.modified = frame.any_held && std::ranges::any_of(kModifiers, Pressed) ? 1 : -1; + } + return frame.modified == 1; + } + + bool IsInputFocused() + { + if (frame.focused == 0) + frame.focused = FocusedInputField() ? 1 : -1; + return frame.focused == 1; + } + + void ClearInputFocus() + { + if (FocusedInputField()) + EventSystem::current()->SetSelectedGameObject(nullptr); + } +} // namespace Key +} // namespace unity diff --git a/libs/unity/src/tracker.cc b/libs/unity/src/tracker.cc new file mode 100644 index 000000000..65e341bc0 --- /dev/null +++ b/libs/unity/src/tracker.cc @@ -0,0 +1,192 @@ +// Owns the registry of tracked instances and the GC hooks that keep it honest. + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace unity::detail +{ +namespace +{ + // Borrowed pointers, keyed by class and every parent class, oldest first. The GC finalizer thread updates this, + // so every access holds the mutex. Never destroyed: the finalizer thread can still run while static destructors + // run at exit. + struct Registry { + std::mutex mutex; + std::unordered_map> objects; + + std::vector* Find(Il2CppClass* klass) + { + const auto it = objects.find(klass); + return it == objects.end() ? nullptr : &it->second; + } + }; + Registry& registry = *new Registry; + + // While the GC marks, the low bit of an object's klass pointer is its mark bit. + Il2CppClass* ClassOf(Il2CppObject* object) + { return reinterpret_cast(reinterpret_cast(object->klass) & ~uintptr_t{1}); } + + bool IsMarked(Il2CppObject* object) + { return (reinterpret_cast(object->klass) & 1) != 0; } + + // Callers hold the registry lock. A base and a derived constructor can both be detoured and both run for one + // object, so tracking twice is a no-op; a second entry would outlive Untrack and hand out a freed object. + void Track(Il2CppObject* object) + { + auto& own = registry.objects[object->klass]; + if (std::ranges::find(own, object) != own.end()) + return; + for (auto* klass = object->klass; klass; klass = klass->parent) + registry.objects[klass].push_back(object); + } + + void Untrack(Il2CppObject* object) + { + for (auto* klass = ClassOf(object); klass; klass = klass->parent) + if (auto* instances = registry.Find(klass)) + std::erase(*instances, object); + } + + // Unity's Boehm fork takes a per-entry "finalize mark proc" as a sixth argument. GC_finalize calls it for every + // unreachable finalized object, and the game's own registrations pass a no-op. Leaving the argument out puts + // register garbage in the entry, which GC_finalize later calls (seen as a call to null during + // ReclaimUnusedMemory). + void FinalizeMarkProcNoop(void*) {} + + using FinalizerCallback = void (*)(void* object, void* client_data); + using RegisterFinalizer = void (*)(uintptr_t obj, FinalizerCallback fn, void* cd, FinalizerCallback* ofn, void** ocd, + void (*mark_proc)(void*)); + // Not exported; found by its prologue. Null when the scan fails, and then only OnDestroy and the liveness pass + // untrack. + RegisterFinalizer register_finalizer = nullptr; + + // A finalizer that was registered before ours, called after untracking. + struct ChainedFinalizer { + FinalizerCallback callback; + void* data; + }; + + // Runs on the GC finalizer thread. `data` is the replaced finalizer, if the object had one. + void OnFinalize(void* object, void* data) + { + if (object) { + std::scoped_lock lock(registry.mutex); + Untrack(static_cast(object)); + } + if (auto* chained = static_cast(data)) { + const auto next = *chained; + delete chained; + next.callback(object, next.data); + } + } + + void WatchFinalizer(Il2CppObject* object) + { + if (!register_finalizer) + return; + const auto address = reinterpret_cast(object); + FinalizerCallback previous = nullptr; + void* previous_data = nullptr; + register_finalizer(address, OnFinalize, nullptr, &previous, &previous_data, FinalizeMarkProcNoop); + // An object has one finalizer slot. Ours is what reports the object gone, and the one it replaced must still + // run, so ours calls it. A base and a derived constructor can both register: then the slot already held ours, + // with any chain, and is put back as it was. + if (previous == OnFinalize) + register_finalizer(address, previous, previous_data, nullptr, nullptr, FinalizeMarkProcNoop); + else if (previous) + register_finalizer(address, OnFinalize, new ChainedFinalizer{previous, previous_data}, nullptr, nullptr, + FinalizeMarkProcNoop); + } + + // After Unity's liveness pass, objects still marked are unreachable from managed code; forget them before their + // native side goes away. + void LivenessFinalize_Hook(auto original, void* state) + { + original(state); + + std::scoped_lock lock(registry.mutex); + std::vector released; + // Every object sits in its own class's list, so checking those lists alone visits each object once. + for (auto& [klass, instances] : registry.objects) + for (auto* object : instances) + if (ClassOf(object) == klass && IsMarked(object)) + released.push_back(object); + for (auto* object : released) + Untrack(object); + } + + RegisterFinalizer FindRegisterFinalizer() + { +#if _WIN32 + auto matches = spud::find_in_module("40 56 57 41 57 48 83 EC ? 83 3D", "GameAssembly.dll"); +#elif SPUD_ARCH_ARM64 + auto matches = spud::find_in_module( + "FF ? 02 D1 FC 6F ? A9 FA 67 ? A9 F8 5F ? A9 F6 57 ? A9 F4 4F ? A9 FD 7B ? A9 FD ? 02 91 E4 0F ? A9", + "GameAssembly.dylib"); +#else + auto matches = spud::find_in_module("55 48 89 E5 41 57 41 56 41 55 41 54 53 48 83 EC ? 4C 89 45 ? 48 89 4D ? 83 3D", + "GameAssembly.dylib"); +#endif + if (matches.empty()) { + il2cpp::Report("[Unity] GC_register_finalizer_inner not found; tracked objects are only dropped on OnDestroy " + "and Unity's liveness pass"); + return nullptr; + } + return reinterpret_cast(matches.front().address()); + } + + std::unordered_set detoured; +} // namespace + +void Constructed(Il2CppObject* object) +{ + if (!object || !object->klass) + return; + WatchFinalizer(object); + std::scoped_lock lock(registry.mutex); + Track(object); +} + +void Destroyed(Il2CppObject* object) +{ + if (!object) + return; + std::scoped_lock lock(registry.mutex); + Untrack(object); +} + +bool FirstDetour(void* native) +{ return detoured.insert(native).second; } + +void StartTracking() +{ + static bool started = false; + if (std::exchange(started, true)) + return; + IL2CPP_DETOUR(il2cpp_unity_liveness_finalize, LivenessFinalize_Hook, "il2cpp_unity_liveness_finalize"); + register_finalizer = FindRegisterFinalizer(); +} + +Il2CppObject* Newest(Il2CppClass* klass) +{ + std::scoped_lock lock(registry.mutex); + auto* objects = klass ? registry.Find(klass) : nullptr; + return objects && !objects->empty() ? objects->back() : nullptr; +} + +std::vector Instances(Il2CppClass* klass) +{ + std::scoped_lock lock(registry.mutex); + auto* objects = klass ? registry.Find(klass) : nullptr; + return objects ? *objects : std::vector{}; +} +} // namespace unity::detail diff --git a/libs/unity/xmake.lua b/libs/unity/xmake.lua new file mode 100644 index 000000000..9ad245902 --- /dev/null +++ b/libs/unity/xmake.lua @@ -0,0 +1,7 @@ +target("unity") + set_kind("static") + add_files("src/*.cc") + add_includedirs("include", {public = true}) + add_deps("il2cpp", {public = true}) + add_packages("spud", {public = true}) + set_exceptions("cxx") diff --git a/macos-dylib/src/main.cc b/macos-dylib/src/main.cc index 4b3be8119..136d69e13 100644 --- a/macos-dylib/src/main.cc +++ b/macos-dylib/src/main.cc @@ -1,25 +1,11 @@ -#include -#include +// The macOS entry point: the loader injects this dylib into the game. + #include -#include -#include "patches/patches.h" +#include "mod.h" -__attribute__((constructor)) -void myconstructor(int argc, const char **argv) +__attribute__((constructor)) void InjectedConstructor(int, const char** argv) { syslog(LOG_ERR, "[+] dylib injected in %s\n", argv[0]); - ApplyPatches(); -} - -void* operator new[](size_t size, const char* /*name*/, int /*flags*/, unsigned /*debugFlags*/, const char* /*file*/, - int /*line*/) -{ - return malloc(size); -} - -void* operator new[](size_t size, size_t /*alignment*/, size_t /*alignmentOffset*/, const char* /*name*/, int /*flags*/, - unsigned /*debugFlags*/, const char* /*file*/, int /*line*/) -{ - return malloc(size); + StartMod(); } diff --git a/mods/src/config.cc b/mods/src/config.cc index 367441c34..154869b6c 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1,69 +1,30 @@ #include "config.h" + #include "file.h" -#include "patches/mapkey.h" -#include "prime/KeyCode.h" -#include "ship_name_match.h" +#include "patches/shortcuts.h" #include "str_utils.h" #include "version.h" -#include -#include +#include "prime/ui.h" + #include +#include -#include "defaultconfig.h" #include -#include +#include +#include #include -#include -#include +#include +#include #include +#include #include -#include -#include -#include - -namespace DCP = DefaultConfig::Patches; -namespace DCA = DefaultConfig::Audio; -namespace DCG = DefaultConfig::Graphics; -namespace DCC = DefaultConfig::Control; -namespace DCU = DefaultConfig::UI; -namespace DCBS = DefaultConfig::Buffs; -namespace DCS = DefaultConfig::Sync; -namespace DCSC = DefaultConfig::SystemConfig; -namespace DCSH = DefaultConfig::Shortcuts; +#include namespace { -constexpr bool is_all_audio_wildcard(std::string_view value) -{ - return value.size() == 3 && (value[0] == 'a' || value[0] == 'A') && (value[1] == 'l' || value[1] == 'L') - && (value[2] == 'l' || value[2] == 'L'); -} - -static_assert(is_all_audio_wildcard("aLl")); -static_assert(!is_all_audio_wildcard(std::string_view{"All\0suffix", 10})); -static_assert(!is_all_audio_wildcard(std::string_view{"\xffll", 3})); - -struct ToastAudioAlertConfig { - int toast_state; - std::string_view config_name; - std::string_view default_sound; - NotificationSound Config::* config_member; -}; - -constexpr auto kToastAudioAlerts = std::to_array({ - {ToastState::Victory, "alert_victory", DCA::alert_victory, &Config::alert_victory}, - {ToastState::Defeat, "alert_defeat", DCA::alert_defeat, &Config::alert_defeat}, - {ToastState::ArmadaCreated, "alert_armada_created", DCA::alert_armada_created, &Config::alert_armada_created}, - {ToastState::ArmadaBattleWon, "alert_armada_battle_won", DCA::alert_armada_battle_won, - &Config::alert_armada_battle_won}, - {ToastState::ArmadaBattleLost, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, - &Config::alert_armada_battle_lost}, -}); -} // namespace - -static const eastl::tuple bannerTypes[] = { - {"All", ToastState::All}, +// Names of ToastState values as written in disabled_banner_types and notify_banner_types. +constexpr std::pair kBannerTypes[] = { {"Standard", ToastState::Standard}, {"FactionWarning", ToastState::FactionWarning}, {"FactionLevelUp", ToastState::FactionLevelUp}, @@ -105,517 +66,173 @@ static const eastl::tuple bannerTypes[] = { {"SurgeTimeLeft", ToastState::SurgeTimeLeft}, }; -bool SyncConfig::enabled(SyncConfig::Type type) const -{ - for (const auto& opt : SyncOptions) { - if (opt.type == type) { - return this->*opt.option; - } - } - - return false; -} - -Config::Config() -{ - Load(); -} - -void Config::Save(const toml::table& config, const std::string_view filename, bool apply_warning) -{ - std::ofstream config_file; - - auto config_path = File::MakePath(filename, true); - config_file.open(config_path); - - if (apply_warning) { - char defaultFile[255], configFile[255]; - snprintf(defaultFile, 255, "%s", File::Default()); - snprintf(configFile, 255, "%s", File::Config()); - - config_file << "#######################################################################\n"; - config_file << "#######################################################################\n"; - config_file << "#### ####\n"; - config_file << "#### NOTE: This file is not the configuration file that is used ####\n"; - config_file << "#### by the STFC Community Mod. It is provided to help ####\n"; - config_file << "#### see what configuration is being used by the runtime ####\n"; - config_file << "#### and any desired settings should be copied to the same ####\n"; - config_file << "#### section in: " << defaultFile << "\n"; - config_file << "#### ####\n"; - config_file << "#### Config in: " << configFile << "\n"; - config_file << "#### ####\n"; - config_file << "#######################################################################\n"; - config_file << "#######################################################################\n\n"; - } - - config_file << config; - config_file.close(); -} - -Config& Config::Get() -{ - static Config config; - return config; -} - -NotificationSound Config::NotificationSoundForToast(int toast_state) const -{ - const auto alert = std::ranges::find(kToastAudioAlerts, toast_state, &ToastAudioAlertConfig::toast_state); - return alert == kToastAudioAlerts.end() ? NotificationSound::None : this->*(alert->config_member); -} - -MissionHudVisibility Config::MissionHudButtonVisibility(std::string_view button_name) const -{ - const auto it = this->mission_hud_buttons.find(std::string(button_name)); - return it == this->mission_hud_buttons.end() ? MissionHudVisibility::Auto : it->second; -} - -#if _WIN32 -static HMONITOR lastMonitor = (HMONITOR)-1; -static float dpi = 1.0f; - -HWND Config::WindowHandle() -{ - static HWND hwnd = nullptr; - - if (hwnd == nullptr) { - DWORD processId = GetCurrentProcessId(); - hwnd = GetTopWindow(nullptr); // Start with the first top-level window - - while (hwnd != nullptr) { - DWORD windowProcessId; - GetWindowThreadProcessId(hwnd, &windowProcessId); - - // Check if the window belongs to the current process and is the main window - if (windowProcessId == processId && GetWindow(hwnd, GW_OWNER) == nullptr && IsWindowVisible(hwnd)) { - break; - } - - hwnd = GetNextWindow(hwnd, GW_HWNDNEXT); // Move to the next top-level window - } - } - - return hwnd; -} - -float Config::RefreshDPI() -{ - lastMonitor = (HMONITOR)-1; - - return Config::GetDPI(); -} - -float Config::GetDPI() -{ - auto activeWindow = GetActiveWindow(); - HMONITOR monitor = MonitorFromWindow(activeWindow, MONITOR_DEFAULTTONEAREST); - - if (monitor != lastMonitor) { - // Get the logical width and height of the monitor - MONITORINFOEX monitorInfoEx; - monitorInfoEx.cbSize = sizeof(monitorInfoEx); - GetMonitorInfo(monitor, &monitorInfoEx); - auto cxLogical = monitorInfoEx.rcMonitor.right - monitorInfoEx.rcMonitor.left; - auto cyLogical = monitorInfoEx.rcMonitor.bottom - monitorInfoEx.rcMonitor.top; - - // Get the physical width and height of the monitor - DEVMODE devMode; - devMode.dmSize = sizeof(devMode); - devMode.dmDriverExtra = 0; - EnumDisplaySettings(monitorInfoEx.szDevice, ENUM_CURRENT_SETTINGS, &devMode); - auto cxPhysical = devMode.dmPelsWidth; - auto cyPhysical = devMode.dmPelsHeight; - - // Calculate the scaling factor - auto horizontalScale = ((double)cxPhysical / (double)cxLogical); - auto verticalScale = ((double)cyPhysical / (double)cyLogical); - - spdlog::trace("Horizontal scaling: {}", horizontalScale); - spdlog::trace("Vertical scaling: {}", verticalScale); - - dpi = horizontalScale; - lastMonitor = monitor; - } - - return dpi; -} -#else -float Config::RefreshDPI() -{ - return Config::GetDPI(); -} - -float Config::GetDPI() -{ - return 1.0f; -} -#endif - -void Config::AdjustUiScale(bool scaleUp) -{ - if (this->ui_scale != 0.0f) { - auto old_scale = this->ui_scale; - auto scale_factor = (scaleUp ? 1.0f : -1.0f) * this->ui_scale_step; - auto new_scale = this->ui_scale + scale_factor; - this->ui_scale = std::clamp(new_scale, 0.1f, 2.0f); - - auto dpi = Config::RefreshDPI(); - spdlog::info("UI has been scaled {}, was {}, now {} (unclamped {}) @ {} DPI Scaling", (scaleUp ? "UP" : "DOWN"), - old_scale, this->ui_scale, new_scale, dpi); - } -} - -void Config::AdjustUiViewerScale(bool scaleUp) -{ - if (this->ui_scale_viewer != 0.0f) { - auto old_scale = this->ui_scale_viewer; - auto scale_factor = (scaleUp ? 1.0f : -1.0f) * this->ui_scale_step; - auto new_scale = this->ui_scale_viewer + (scale_factor * 0.25f); - this->ui_scale_viewer = std::clamp(new_scale, 0.1f, 2.0f); - - spdlog::info("UI Viewer has been scaled {}, was {}, now {} (unclamped {})", (scaleUp ? "UP" : "DOWN"), old_scale, - this->ui_scale_viewer, new_scale); - } -} - -void Config::AdjustUiShipScale(bool scaleUp) -{ - const auto old_scale = this->ui_scale_ship; - const auto scale_factor = (scaleUp ? 1.0f : -1.0f) * this->ui_scale_step; - const auto new_scale = this->ui_scale_ship + scale_factor; - this->ui_scale_ship = std::clamp(new_scale, 0.1f, 20.0f); - - spdlog::info("System ship models have been scaled {}, was {}, now {} (unclamped {})", (scaleUp ? "UP" : "DOWN"), - old_scale, this->ui_scale_ship, new_scale); - ApplyUiShipScaleToLoadedShips(old_scale, this->ui_scale_ship); -} - -inline std::string mask_token(const std::string& token) -{ - if (token.size() > 21) { - std::string masked = token; - for (size_t i = 9; i < token.size() - 12; ++i) { - if (masked[i] != '-') { - masked[i] = '*'; - } - } - return masked; - } - - return token; -} - -std::string get_config_type_as_string(const toml::node_type type) -{ - switch (type) { - case toml::node_type::none: - return "Not-a-node."; - case toml::node_type::table: - return "toml::table."; - case toml::node_type::array: - return "toml::array."; - case toml::node_type::string: - return "toml::value."; - case toml::node_type::integer: - return "toml::value."; - case toml::node_type::floating_point: - return "toml::value."; - case toml::node_type::boolean: - return "toml::value."; - case toml::node_type::date: - return "toml::value."; - case toml::node_type::time: - return "toml::value