poe2-bot/src/Roboto.Input/SendInputController.cs
2026-03-05 11:26:30 -05:00

300 lines
8.6 KiB
C#

using System.Runtime.InteropServices;
using Roboto.Core;
using Serilog;
namespace Roboto.Input;
/// <summary>
/// Fallback input controller using Win32 SendInput with KEYEVENTF_SCANCODE.
/// Games read scan codes, so this works for POE2 without the Interception driver.
/// </summary>
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<INPUT>();
[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);
}