using System.Runtime.InteropServices; using Roboto.Core; using Serilog; namespace Roboto.Input; /// /// Fallback input controller using Win32 SendInput with KEYEVENTF_SCANCODE. /// Games read scan codes, so this works for POE2 without the Interception driver. /// public sealed partial class SendInputController : IInputController { private static readonly Random Rng = new(); private readonly Humanizer? _humanizer; public bool IsInitialized { get; private set; } public SendInputController(Humanizer? humanizer = null) { _humanizer = humanizer; } public bool Initialize() { IsInitialized = true; Log.Information("SendInput controller initialized (fallback)"); return true; } // ── Keyboard ── public void KeyDown(ushort scanCode) { var input = MakeKeyScanInput(scanCode, keyUp: false); SendInput(1, [input], INPUT_SIZE); } public void KeyUp(ushort scanCode) { var input = MakeKeyScanInput(scanCode, keyUp: true); SendInput(1, [input], INPUT_SIZE); } public void KeyPress(ushort scanCode, int holdMs = 50) { if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; holdMs = _humanizer.GaussianDelay(holdMs); _humanizer.RecordAction(); } KeyDown(scanCode); Thread.Sleep(holdMs); KeyUp(scanCode); } // ── Mouse movement ── public void MouseMoveTo(int x, int y) { SetCursorPos(x, y); } public void MouseMoveBy(int dx, int dy) { if (!GetCursorPos(out var pt)) return; SetCursorPos(pt.X + dx, pt.Y + dy); } public void SmoothMoveTo(int x, int y) { if (!GetCursorPos(out var pt)) { MouseMoveTo(x, y); return; } var dx = (double)(x - pt.X); var dy = (double)(y - pt.Y); var distance = Math.Sqrt(dx * dx + dy * dy); if (distance < 15) { MouseMoveTo(x, y); return; } var perpX = -dy / distance; var perpY = dx / distance; var spread = distance * 0.15; var cp1X = pt.X + dx * 0.3 + perpX * (Rng.NextDouble() - 0.5) * spread; var cp1Y = pt.Y + dy * 0.3 + perpY * (Rng.NextDouble() - 0.5) * spread; var cp2X = pt.X + dx * 0.7 + perpX * (Rng.NextDouble() - 0.5) * spread; var cp2Y = pt.Y + dy * 0.7 + perpY * (Rng.NextDouble() - 0.5) * spread; var steps = Math.Clamp((int)Math.Round(distance / 15), 10, 40); for (var i = 1; i <= steps; i++) { var t = EaseInOutQuad((double)i / steps); var (bx, by) = CubicBezier(t, pt.X, pt.Y, cp1X, cp1Y, cp2X, cp2Y, x, y); MouseMoveTo((int)Math.Round(bx), (int)Math.Round(by)); Thread.Sleep(2 + Rng.Next(3)); } MouseMoveTo(x, y); } // ── Mouse clicks ── public void LeftClick(int x, int y) { if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; (x, y) = _humanizer.JitterPosition(x, y); Thread.Sleep(_humanizer.GaussianDelay(10)); _humanizer.RecordAction(); } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); MouseClick(MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, _humanizer?.GaussianDelay(50) ?? 50); } public void RightClick(int x, int y) { if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; (x, y) = _humanizer.JitterPosition(x, y); Thread.Sleep(_humanizer.GaussianDelay(10)); _humanizer.RecordAction(); } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); MouseClick(MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, _humanizer?.GaussianDelay(50) ?? 50); } public void MiddleClick(int x, int y) { if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; (x, y) = _humanizer.JitterPosition(x, y); Thread.Sleep(_humanizer.GaussianDelay(10)); _humanizer.RecordAction(); } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); MouseClick(MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, _humanizer?.GaussianDelay(50) ?? 50); } public void LeftDown() { var input = MakeMouseInput(MOUSEEVENTF_LEFTDOWN); SendInput(1, [input], INPUT_SIZE); } public void LeftUp() { var input = MakeMouseInput(MOUSEEVENTF_LEFTUP); SendInput(1, [input], INPUT_SIZE); } public void RightDown() { var input = MakeMouseInput(MOUSEEVENTF_RIGHTDOWN); SendInput(1, [input], INPUT_SIZE); } public void RightUp() { var input = MakeMouseInput(MOUSEEVENTF_RIGHTUP); SendInput(1, [input], INPUT_SIZE); } // ── Private helpers ── private void MouseClick(uint downFlag, uint upFlag, int holdMs) { var down = MakeMouseInput(downFlag); var up = MakeMouseInput(upFlag); SendInput(1, [down], INPUT_SIZE); Thread.Sleep(holdMs); SendInput(1, [up], INPUT_SIZE); } private static double EaseInOutQuad(double t) => t < 0.5 ? 2 * t * t : 1 - Math.Pow(-2 * t + 2, 2) / 2; private static (double X, double Y) CubicBezier(double t, double p0x, double p0y, double p1x, double p1y, double p2x, double p2y, double p3x, double p3y) { var u = 1 - t; var u2 = u * u; var u3 = u2 * u; var t2 = t * t; var t3 = t2 * t; return ( u3 * p0x + 3 * u2 * t * p1x + 3 * u * t2 * p2x + t3 * p3x, u3 * p0y + 3 * u2 * t * p1y + 3 * u * t2 * p2y + t3 * p3y ); } // ── Win32 SendInput P/Invoke ── private const int INPUT_KEYBOARD = 1; private const int INPUT_MOUSE = 0; private const uint KEYEVENTF_SCANCODE = 0x0008; private const uint KEYEVENTF_KEYUP = 0x0002; private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; private const uint MOUSEEVENTF_LEFTUP = 0x0004; private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008; private const uint MOUSEEVENTF_RIGHTUP = 0x0010; private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020; private const uint MOUSEEVENTF_MIDDLEUP = 0x0040; private static readonly int INPUT_SIZE = Marshal.SizeOf(); [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] private struct INPUT { public int type; public INPUT_UNION union; } [StructLayout(LayoutKind.Explicit)] private struct INPUT_UNION { [FieldOffset(0)] public KEYBDINPUT ki; [FieldOffset(0)] public MOUSEINPUT mi; } [StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public nint dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public nint dwExtraInfo; } private static INPUT MakeKeyScanInput(ushort scanCode, bool keyUp) { var flags = KEYEVENTF_SCANCODE; if (keyUp) flags |= KEYEVENTF_KEYUP; return new INPUT { type = INPUT_KEYBOARD, union = new INPUT_UNION { ki = new KEYBDINPUT { wVk = 0, wScan = scanCode, dwFlags = flags, time = 0, dwExtraInfo = 0, } } }; } private static INPUT MakeMouseInput(uint flags) { return new INPUT { type = INPUT_MOUSE, union = new INPUT_UNION { mi = new MOUSEINPUT { dwFlags = flags, } } }; } [LibraryImport("user32.dll", SetLastError = true)] private static partial uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SetCursorPos(int x, int y); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool GetCursorPos(out POINT lpPoint); }