From 0ec422debac9039155168dd3c70f0d10c7201a65 Mon Sep 17 00:00:00 2001 From: iidamie <127737368+iidamie@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:49:22 +0800 Subject: [PATCH 1/2] fix(android): add input broadcast APIs --- .../Native/AndroidInput.cs | 44 ++- .../Native/InputEvents.cs | 310 ++++++++++++++++++ .../UI/ImGuiInputHandler.cs | 107 ++---- .../RuntimeAbstractions/RuntimeManager.cs | 27 ++ 4 files changed, 403 insertions(+), 85 deletions(-) create mode 100644 StArray.ModManager.Android/Native/InputEvents.cs diff --git a/StArray.ModManager.Android/Native/AndroidInput.cs b/StArray.ModManager.Android/Native/AndroidInput.cs index 3699bca..d67c91d 100644 --- a/StArray.ModManager.Android/Native/AndroidInput.cs +++ b/StArray.ModManager.Android/Native/AndroidInput.cs @@ -33,7 +33,7 @@ public enum KeyAction Multiple = 2, } - /// 触摸动作(主动作需用 ActionMask 提取) + /// 触摸动作(主动作需先去除指针索引位) public enum MotionAction { Down = 0, @@ -43,11 +43,12 @@ public enum MotionAction Outside = 4, PointerDown = 5, PointerUp = 6, - HoverEnter = 7, - HoverMove = 8, - HoverExit = 9, - ButtonPress = 10, - ButtonRelease = 11, + HoverMove = 7, + Scroll = 8, + HoverEnter = 9, + HoverExit = 10, + ButtonPress = 11, + ButtonRelease = 12, } /// Meta 键状态 @@ -71,6 +72,13 @@ public static class MotionMask public const int PointerIndexShift = 8; } + // Keep integer helpers available to the timestamp broadcast path. The raw + // action is read only once per native event, avoiding repeated P/Invoke + // calls on the Android input thread. + public const int ActionMask = MotionMask.Action; + public const int ActionPointerIndexMask = MotionMask.PointerIndex; + public const int ActionPointerIndexShift = MotionMask.PointerIndexShift; + // ======================== 不透明句柄结构体 ======================== /// 不透明输入事件句柄 @@ -156,21 +164,39 @@ int handled [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] public static extern int AMotionEvent_getPointerId(IntPtr ev, int pointerIndex); + /// + /// 事件发生时刻,单位纳秒,时钟源为 CLOCK_MONOTONIC。 + /// 该时间戳独立于渲染帧,可用于还原触摸真实发生时刻。 + /// + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern long AMotionEvent_getEventTime(IntPtr ev); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern long AMotionEvent_getDownTime(IntPtr ev); + // ======================== 辅助方法(扩展风格) ======================== + /// 从原始动作值取出主动作。 + public static MotionAction GetMainAction(int rawAction) + => (MotionAction)(rawAction & ActionMask); + + /// 从原始动作值取出指针索引。 + public static int GetPointerIndex(int rawAction) + => (rawAction & ActionPointerIndexMask) >> ActionPointerIndexShift; + /// 获取触摸事件的主动作(去除指针索引) public static MotionAction GetMainAction(this IntPtr ev) { if (AInputEvent_getType(ev) != EventType.Motion) throw new InvalidOperationException("事件不是 Motion 类型"); int raw = AMotionEvent_getAction(ev); - return (MotionAction)(raw & MotionMask.Action); + return GetMainAction(raw); } /// 获取触摸事件的指针索引(用于多点触控) public static int GetPointerIndex(this IntPtr ev) { int raw = AMotionEvent_getAction(ev); - return (raw & MotionMask.PointerIndex) >> MotionMask.PointerIndexShift; + return GetPointerIndex(raw); } -} \ No newline at end of file +} diff --git a/StArray.ModManager.Android/Native/InputEvents.cs b/StArray.ModManager.Android/Native/InputEvents.cs new file mode 100644 index 0000000..4dca2df --- /dev/null +++ b/StArray.ModManager.Android/Native/InputEvents.cs @@ -0,0 +1,310 @@ +using System.Diagnostics; +using StArray.ModManager.Manager; + +namespace StArray.ModManager.Android.Native; + +/// 一次完整触摸事件的快照。所有字段在广播时即已从原生事件读出。 +/// 主动作,已去除指针索引 +/// 指针索引 +/// 稳定的指针 ID,比索引更适合跨事件跟踪多指输入 +/// 事件发生时刻,纳秒,时钟源为 CLOCK_MONOTONIC +/// 触点 X 坐标(像素) +/// 触点 Y 坐标(像素) +public readonly record struct TouchEventInfo( + AndroidInput.MotionAction Action, + int PointerIndex, + int PointerId, + long EventTimeNanos, + float X, + float Y) +{ + // Source compatibility for mods built against the first broadcast API. + public TouchEventInfo( + AndroidInput.MotionAction action, + int pointerIndex, + long eventTimeNanos, + float x, + float y) + : this(action, pointerIndex, -1, eventTimeNanos, x, y) + { + } +} + +/// +/// 只包含异步输入所需字段的原始触摸快照。 +/// +/// +/// 该事件不读取坐标,且只广播 Down/Up/Cancel。高 KPS 时订阅方不需要为 Move +/// 事件读取坐标或创建完整手势快照。 +/// +public readonly record struct TouchTimestampInfo( + AndroidInput.MotionAction Action, + int PointerId, + long EventTimeNanos); + +/// +/// 输入事件广播点。原生输入 Hook 只负责解析一次事件,订阅方在自己的队列中异步处理。 +/// +/// +/// 回调运行在 Android 输入分发线程,不是 Unity 主线程。订阅方只能做廉价的值类型快照 +/// 和入队操作,不能访问 Unity 对象或执行 IL2CPP 游戏逻辑。 +/// +public static class InputEvents +{ + private const string LogTag = nameof(InputEvents); + private const long DuplicateWindowMilliseconds = 8L; + + private static Action? s_onTouch; + private static Action? s_onTouchTimestamp; + private static int s_touchSubscriberCount; + private static int s_touchTimestampSubscriberCount; + private static int s_faultLogged; + + private static readonly object DedupLock = new(); + private static int s_lastRawAction; + private static int s_lastPointerIndex; + private static int s_lastPointerCount; + private static int s_lastPointerId; + private static long s_lastEventTimeNanos; + private static long s_lastDispatchTicks; + + /// 是否已有任一类订阅者。 + public static bool HasSubscribers => + Volatile.Read(ref s_touchSubscriberCount) > 0 + || Volatile.Read(ref s_touchTimestampSubscriberCount) > 0; + + /// 完整触摸事件广播,保留坐标和 Move 事件。 + public static event Action? OnTouch + { + add + { + if (value == null) return; + lock (DedupLock) + { + s_onTouch += value; + Interlocked.Increment(ref s_touchSubscriberCount); + } + } + remove + { + if (value == null) return; + lock (DedupLock) + { + s_onTouch -= value; + Interlocked.Decrement(ref s_touchSubscriberCount); + } + } + } + + /// + /// 异步输入时间戳快速广播,只处理 Down、PointerDown、Up、PointerUp 和 Cancel。 + /// + public static event Action? OnTouchTimestamp + { + add + { + if (value == null) return; + lock (DedupLock) + { + s_onTouchTimestamp += value; + Interlocked.Increment(ref s_touchTimestampSubscriberCount); + } + } + remove + { + if (value == null) return; + lock (DedupLock) + { + s_onTouchTimestamp -= value; + Interlocked.Decrement(ref s_touchTimestampSubscriberCount); + } + } + } + + /// + /// 从原生 AInputEvent 解析并广播。输入事件只在这里读取一次,避免每个 Mod 重复访问 + /// 原生对象;重复的同一事件只在广播层过滤一次。 + /// + internal static void RaiseFrom(nint inputEvent) + { + Action? handlers; + Action? timestampHandlers; + lock (DedupLock) + { + handlers = s_onTouch; + timestampHandlers = s_onTouchTimestamp; + } + + if ((handlers == null && timestampHandlers == null) || inputEvent == 0) + return; + + try + { + if (AndroidInput.AInputEvent_getType(inputEvent) != AndroidInput.EventType.Motion) + return; + + int rawAction = AndroidInput.AMotionEvent_getAction(inputEvent); + AndroidInput.MotionAction action = AndroidInput.GetMainAction(rawAction); + int pointerIndex = AndroidInput.GetPointerIndex(rawAction); + bool timestampAction = action is AndroidInput.MotionAction.Down + or AndroidInput.MotionAction.PointerDown + or AndroidInput.MotionAction.Up + or AndroidInput.MotionAction.PointerUp + or AndroidInput.MotionAction.Cancel; + + // AsyncInput subscribes only to the timestamp channel. Do not + // inspect Move coordinates, pointer IDs, or pointer counts on the + // hot path when no full-gesture subscriber exists. + if (!timestampAction && handlers == null) + return; + + int pointerCount = AndroidInput.AMotionEvent_getPointerCount(inputEvent); + long eventTimeNanos = AndroidInput.AMotionEvent_getEventTime(inputEvent); + int pointerId = action == AndroidInput.MotionAction.Cancel + ? -1 + : AndroidInput.AMotionEvent_getPointerId(inputEvent, pointerIndex); + + if (IsDuplicate( + rawAction, + pointerIndex, + pointerCount, + pointerId, + eventTimeNanos)) + return; + + if (timestampHandlers != null && timestampAction) + { + DispatchTimestampHandlers( + timestampHandlers, + new TouchTimestampInfo(action, pointerId, eventTimeNanos)); + } + + if (handlers == null) + return; + + TouchEventInfo info = new( + action, + pointerIndex, + pointerId, + eventTimeNanos, + AndroidInput.AMotionEvent_getX(inputEvent, pointerIndex), + AndroidInput.AMotionEvent_getY(inputEvent, pointerIndex)); + DispatchTouchHandlers(handlers, info); + } + catch (Exception exception) + { + LogOnce($"Failed to read native input event: {exception}"); + } + } + + private static bool IsDuplicate( + int rawAction, + int pointerIndex, + int pointerCount, + int pointerId, + long eventTimeNanos) + { + long now = Stopwatch.GetTimestamp(); + long windowTicks = Math.Max( + 1L, + Stopwatch.Frequency * DuplicateWindowMilliseconds / 1000L); + + lock (DedupLock) + { + long elapsed = now - s_lastDispatchTicks; + bool sameEventPayload = s_lastRawAction == rawAction + && s_lastPointerIndex == pointerIndex + && s_lastPointerCount == pointerCount + && s_lastPointerId == pointerId + && s_lastEventTimeNanos == eventTimeNanos; + bool duplicate = sameEventPayload + && elapsed >= 0L + && elapsed <= windowTicks; + + if (duplicate) + return true; + + s_lastRawAction = rawAction; + s_lastPointerIndex = pointerIndex; + s_lastPointerCount = pointerCount; + s_lastPointerId = pointerId; + s_lastEventTimeNanos = eventTimeNanos; + s_lastDispatchTicks = now; + return false; + } + } + + private static void DispatchTimestampHandlers( + Action handlers, + TouchTimestampInfo info) + { + if (Volatile.Read(ref s_touchTimestampSubscriberCount) == 1) + { + try + { + handlers(info); + } + catch (Exception exception) + { + LogOnce($"Touch timestamp subscriber threw: {exception}"); + } + return; + } + + foreach (Delegate handler in handlers.GetInvocationList()) + { + try + { + ((Action)handler)(info); + } + catch (Exception exception) + { + LogOnce($"Touch timestamp subscriber threw: {exception}"); + } + } + } + + private static void DispatchTouchHandlers( + Action handlers, + TouchEventInfo info) + { + if (Volatile.Read(ref s_touchSubscriberCount) == 1) + { + try + { + handlers(info); + } + catch (Exception exception) + { + LogOnce($"Touch event subscriber threw: {exception}"); + } + return; + } + + foreach (Delegate handler in handlers.GetInvocationList()) + { + try + { + ((Action)handler)(info); + } + catch (Exception exception) + { + LogOnce($"Touch event subscriber threw: {exception}"); + } + } + } + + private static void LogOnce(string message) + { + if (Interlocked.Exchange(ref s_faultLogged, 1) != 0) + return; + try + { + Logger.Error(LogTag, $"{message} (further occurrences suppressed)"); + } + catch + { + // Never allow an exception to escape back through the native input stack. + } + } +} diff --git a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs index 5cd20ae..10ca52d 100644 --- a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs +++ b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ImGuiNET; using StArray.ModManager.Android.Native; @@ -68,86 +67,42 @@ public static void InstallInputHooks() IsInitialized = true; } - /* - /// 触摸事件 Hook 回调 - [NativeHook("libinput.so","_ZN7android13InputConsumer14consumeSamplesEPNS_26InputEventFactoryInterfaceERNS0_5BatchEmPjPPNS_10InputEventE")] - public unsafe static long OnConsumeSamples(void* thiz,void* factory, IntPtr batch, - ulong count, uint* outSeq, void** outEvent) + /// 在 Android 输入消费完成后读取本次输出事件。 + [NativeHook("libinput.so", "_ZN7android13InputConsumer7consumeEPNS_26InputEventFactoryInterfaceEblPjPPNS_10InputEventE", + Convention = CallingConvention.Cdecl)] + public unsafe static int OnConsume( + void* thiz, + void* factory, + bool consumeBatches, + long frameTime, + uint* outSeq, + void** outEvent) { - var result = OnConsumeSamplesOriginal(thiz,factory, batch, count, outSeq, outEvent); - if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); - return result; - } - - [NativeHook("libinput.so","_ZN7android13InputConsumer7consumeEPNS_26InputEventFactoryInterfaceEblPjPPNS_10InputEventE")] - public unsafe static long OnConsume(void* thiz, void* factory, bool consumeBatches, ulong frameTime, uint* outSeq, void** outEvent) - { - var result = OnConsumeOriginal(thiz, factory, consumeBatches, frameTime, outSeq, outEvent); - if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); - return result; - }*/ - - [NativeHook("GetInitializeMotionEventAddress")] - public unsafe static bool OnInitializeMotionEvent(void* @event, void* message) - { - var result = OnInitializeMotionEventOriginal(@event, message); - var x = AndroidInput.AMotionEvent_getX(new(@event), 0); - var y = AndroidInput.AMotionEvent_getY(new(@event), 0); - ImGuiImplAndroid.HandleInputEvent(new IntPtr(@event)); + var result = OnConsumeOriginal( + thiz, + factory, + consumeBatches, + frameTime, + outSeq, + outEvent); + if (outEvent != null && *outEvent != null) + DispatchInputEvent(new IntPtr(*outEvent)); return result; } - private static nint GetInitializeMotionEventAddress() + /// + /// 把一个原生输入事件同时送往 ImGui 和 订阅方。 + /// + /// + /// 输入广播不依赖 ImGui 上下文是否已经初始化;这样异步输入 Mod 可以在 + /// 菜单、暂停和非游玩阶段继续收到原始事件并自行决定是否消费。 + /// + private static void DispatchInputEvent(IntPtr inputEvent) { - byte?[] sig = NativeFuncResolver.ParseHexPattern( - "e8 0f 19 fc fd 7b 01 a9 fc 6f 02 a9 fa 67 03 a9 " + - "f8 5f 04 a9 f6 57 05 a9 f4 4f 06 a9 fd 43 00 91"); - var r = new NativeFuncResolver("/system/lib64/libinput.so"); - long rva = r.FindSymbolRva("_ZN7android13InputConsumer21initializeMotionEventEPNS_11MotionEventEPKNS_12InputMessageE"); - try - { - if (rva < 0) - { - var text = r.TextBytes; - long textAddr = r.TextBaseAddress; - int pos = 0; - while ((pos = NativeFuncResolver.Search(text, sig, pos)) >= 0) - { - int ctxEnd = Math.Min(pos + 60, text.Length); - bool hasMrs = false, hasLdr = false; - for (int j = pos + 32; j <= ctxEnd - 4; j += 4) - { - int inst = BitConverter.ToInt32(text.AsSpan(j)); - if (!hasMrs && (inst & 0xffffffe0) == 0xd53bd040) - hasMrs = true; - if (!hasLdr && (inst & 0xffe003e0) == 0xb9400020) - { - int imm12 = (inst >> 10) & 0xfff; - if (imm12 * 4 == 0xc) hasLdr = true; - } - if (hasMrs && hasLdr) break; - } - if (hasMrs && hasLdr) - { - rva = textAddr + pos; - if (pos >= 4) - { - int prev = BitConverter.ToInt32(text.AsSpan(pos - 4)); - if (prev == unchecked((int)0xd503233f)) rva -= 4; - } - break; - } - pos++; - } - if (rva < 0) throw new KeyNotFoundException("initializeMotionEvent not found by signature."); - } - } - catch (Exception ex) - { - Logger.Error(nameof(ImGuiInputHandler), ex.ToString()); - } - r.Load(); - return r.GetFuncPtr(rva); + if (InputEvents.HasSubscribers) + InputEvents.RaiseFrom(inputEvent); + if (IsInitialized) + ImGuiImplAndroid.HandleInputEvent(inputEvent); } private static JavaClass? s_utilsClass; diff --git a/StArray.ModManager/RuntimeAbstractions/RuntimeManager.cs b/StArray.ModManager/RuntimeAbstractions/RuntimeManager.cs index 9e72119..eb65f63 100644 --- a/StArray.ModManager/RuntimeAbstractions/RuntimeManager.cs +++ b/StArray.ModManager/RuntimeAbstractions/RuntimeManager.cs @@ -72,6 +72,33 @@ public static RuntimeBackend Detect() return null; } + /// + /// 从运行时对象取得其真实类型。 + /// + /// + /// 泛型容器只有在对象实例创建后才能拿到具体方法表。调用方可缓存返回类型的 + /// Add/Clear 等方法,避免每次事件通过字符串重新解析。 + /// + public static IRuntimeClass? GetObjectClass(nint objectPtr) + { + if (objectPtr == 0) + return null; + + if (IsIl2Cpp) + { + nint klass = Il2CppFunctions.il2cpp_object_get_class(objectPtr); + return klass == 0 ? null : new Il2CppClass(klass); + } + + if (IsMono) + { + nint klass = MonoFunctions.MonoObjectGetClass(objectPtr); + return klass == 0 ? null : new MonoClass(klass); + } + + return null; + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string lpModuleName); From d95b75aa11fea82a148267dbe4f49a09ef8a6f0f Mon Sep 17 00:00:00 2001 From: iidamie <127737368+iidamie@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:21:18 +0800 Subject: [PATCH 2/2] fix(android): preserve existing input hook --- .../UI/ImGuiInputHandler.cs | 109 +++++++++++++----- 1 file changed, 78 insertions(+), 31 deletions(-) diff --git a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs index 10ca52d..2eb0cd4 100644 --- a/StArray.ModManager.Android/UI/ImGuiInputHandler.cs +++ b/StArray.ModManager.Android/UI/ImGuiInputHandler.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ImGuiNET; using StArray.ModManager.Android.Native; @@ -67,42 +68,88 @@ public static void InstallInputHooks() IsInitialized = true; } - /// 在 Android 输入消费完成后读取本次输出事件。 - [NativeHook("libinput.so", "_ZN7android13InputConsumer7consumeEPNS_26InputEventFactoryInterfaceEblPjPPNS_10InputEventE", - Convention = CallingConvention.Cdecl)] - public unsafe static int OnConsume( - void* thiz, - void* factory, - bool consumeBatches, - long frameTime, - uint* outSeq, - void** outEvent) + /* + /// 触摸事件 Hook 回调 + [NativeHook("libinput.so","_ZN7android13InputConsumer14consumeSamplesEPNS_26InputEventFactoryInterfaceERNS0_5BatchEmPjPPNS_10InputEventE")] + public unsafe static long OnConsumeSamples(void* thiz,void* factory, IntPtr batch, + ulong count, uint* outSeq, void** outEvent) { - var result = OnConsumeOriginal( - thiz, - factory, - consumeBatches, - frameTime, - outSeq, - outEvent); - if (outEvent != null && *outEvent != null) - DispatchInputEvent(new IntPtr(*outEvent)); + var result = OnConsumeSamplesOriginal(thiz,factory, batch, count, outSeq, outEvent); + if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); return result; } - - /// - /// 把一个原生输入事件同时送往 ImGui 和 订阅方。 - /// - /// - /// 输入广播不依赖 ImGui 上下文是否已经初始化;这样异步输入 Mod 可以在 - /// 菜单、暂停和非游玩阶段继续收到原始事件并自行决定是否消费。 - /// - private static void DispatchInputEvent(IntPtr inputEvent) + + [NativeHook("libinput.so","_ZN7android13InputConsumer7consumeEPNS_26InputEventFactoryInterfaceEblPjPPNS_10InputEventE")] + public unsafe static long OnConsume(void* thiz, void* factory, bool consumeBatches, ulong frameTime, uint* outSeq, void** outEvent) { + var result = OnConsumeOriginal(thiz, factory, consumeBatches, frameTime, outSeq, outEvent); + if (IsInitialized && *outEvent != null) ImGuiImplAndroid.HandleInputEvent(new IntPtr(*outEvent)); + return result; + }*/ + + [NativeHook("GetInitializeMotionEventAddress")] + public unsafe static bool OnInitializeMotionEvent(void* @event, void* message) + { + var result = OnInitializeMotionEventOriginal(@event, message); + var x = AndroidInput.AMotionEvent_getX(new(@event), 0); + var y = AndroidInput.AMotionEvent_getY(new(@event), 0); if (InputEvents.HasSubscribers) - InputEvents.RaiseFrom(inputEvent); - if (IsInitialized) - ImGuiImplAndroid.HandleInputEvent(inputEvent); + InputEvents.RaiseFrom(new IntPtr(@event)); + ImGuiImplAndroid.HandleInputEvent(new IntPtr(@event)); + return result; + } + + private static nint GetInitializeMotionEventAddress() + { + byte?[] sig = NativeFuncResolver.ParseHexPattern( + "e8 0f 19 fc fd 7b 01 a9 fc 6f 02 a9 fa 67 03 a9 " + + "f8 5f 04 a9 f6 57 05 a9 f4 4f 06 a9 fd 43 00 91"); + var r = new NativeFuncResolver("/system/lib64/libinput.so"); + long rva = r.FindSymbolRva("_ZN7android13InputConsumer21initializeMotionEventEPNS_11MotionEventEPKNS_12InputMessageE"); + try + { + if (rva < 0) + { + var text = r.TextBytes; + long textAddr = r.TextBaseAddress; + int pos = 0; + while ((pos = NativeFuncResolver.Search(text, sig, pos)) >= 0) + { + int ctxEnd = Math.Min(pos + 60, text.Length); + bool hasMrs = false, hasLdr = false; + for (int j = pos + 32; j <= ctxEnd - 4; j += 4) + { + int inst = BitConverter.ToInt32(text.AsSpan(j)); + if (!hasMrs && (inst & 0xffffffe0) == 0xd53bd040) + hasMrs = true; + if (!hasLdr && (inst & 0xffe003e0) == 0xb9400020) + { + int imm12 = (inst >> 10) & 0xfff; + if (imm12 * 4 == 0xc) hasLdr = true; + } + if (hasMrs && hasLdr) break; + } + if (hasMrs && hasLdr) + { + rva = textAddr + pos; + if (pos >= 4) + { + int prev = BitConverter.ToInt32(text.AsSpan(pos - 4)); + if (prev == unchecked((int)0xd503233f)) rva -= 4; + } + break; + } + pos++; + } + if (rva < 0) throw new KeyNotFoundException("initializeMotionEvent not found by signature."); + } + } + catch (Exception ex) + { + Logger.Error(nameof(ImGuiInputHandler), ex.ToString()); + } + r.Load(); + return r.GetFuncPtr(rva); } private static JavaClass? s_utilsClass;