poe2-bot/src-old/game/InputSender.ts
2026-02-13 01:12:11 -05:00

342 lines
9.9 KiB
TypeScript

import koffi from 'koffi';
import { sleep, randomDelay } from '../util/sleep.js';
// Win32 POINT struct for GetCursorPos
const POINT = koffi.struct('POINT', { x: 'int32', y: 'int32' });
// Win32 INPUT struct on x64 is 40 bytes:
// type (4) + pad (4) + union (32)
// MOUSEINPUT is 32 bytes (the largest union member)
// KEYBDINPUT is 24 bytes, so needs 8 bytes trailing pad in the union
//
// We define flat structs that match the exact memory layout,
// then cast with koffi.as() when calling SendInput.
const INPUT_KEYBOARD = koffi.struct('INPUT_KEYBOARD', {
type: 'uint32', // offset 0
_pad0: 'uint32', // offset 4 (alignment for union at offset 8)
wVk: 'uint16', // offset 8
wScan: 'uint16', // offset 10
dwFlags: 'uint32', // offset 12
time: 'uint32', // offset 16
_pad1: 'uint32', // offset 20 (alignment for dwExtraInfo)
dwExtraInfo: 'uint64', // offset 24
_pad2: koffi.array('uint8', 8), // offset 32, pad to 40 bytes total
});
const INPUT_MOUSE = koffi.struct('INPUT_MOUSE', {
type: 'uint32', // offset 0
_pad0: 'uint32', // offset 4 (alignment for union at offset 8)
dx: 'int32', // offset 8
dy: 'int32', // offset 12
mouseData: 'uint32', // offset 16
dwFlags: 'uint32', // offset 20
time: 'uint32', // offset 24
_pad1: 'uint32', // offset 28 (alignment for dwExtraInfo)
dwExtraInfo: 'uint64', // offset 32
});
// INPUT_MOUSE is already 40 bytes, no trailing pad needed
const user32 = koffi.load('user32.dll');
const SendInput = user32.func('SendInput', 'uint32', ['uint32', 'void *', 'int32']);
const MapVirtualKeyW = user32.func('MapVirtualKeyW', 'uint32', ['uint32', 'uint32']);
const GetSystemMetrics = user32.func('GetSystemMetrics', 'int32', ['int32']);
const GetCursorPos = user32.func('GetCursorPos', 'int32', ['_Out_ POINT *']);
// Constants
const INPUT_MOUSE_TYPE = 0;
const INPUT_KEYBOARD_TYPE = 1;
const KEYEVENTF_SCANCODE = 0x0008;
const KEYEVENTF_KEYUP = 0x0002;
const KEYEVENTF_UNICODE = 0x0004;
// Mouse flags
const MOUSEEVENTF_MOVE = 0x0001;
const MOUSEEVENTF_LEFTDOWN = 0x0002;
const MOUSEEVENTF_LEFTUP = 0x0004;
const MOUSEEVENTF_RIGHTDOWN = 0x0008;
const MOUSEEVENTF_RIGHTUP = 0x0010;
const MOUSEEVENTF_ABSOLUTE = 0x8000;
// System metrics
const SM_CXSCREEN = 0;
const SM_CYSCREEN = 1;
// Virtual key codes
export const VK = {
RETURN: 0x0d,
CONTROL: 0x11,
MENU: 0x12, // Alt
SHIFT: 0x10,
ESCAPE: 0x1b,
TAB: 0x09,
SPACE: 0x20,
DELETE: 0x2e,
BACK: 0x08,
V: 0x56,
A: 0x41,
C: 0x43,
I: 0x49,
} as const;
// Size to pass to SendInput (must be sizeof(INPUT) = 40 on x64)
const INPUT_SIZE = koffi.sizeof(INPUT_MOUSE); // 40
// Bézier curve helpers for natural mouse movement
function easeInOutQuad(t: number): number {
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
}
interface Point {
x: number;
y: number;
}
function cubicBezier(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point {
const u = 1 - t;
const u2 = u * u;
const u3 = u2 * u;
const t2 = t * t;
const t3 = t2 * t;
return {
x: u3 * p0.x + 3 * u2 * t * p1.x + 3 * u * t2 * p2.x + t3 * p3.x,
y: u3 * p0.y + 3 * u2 * t * p1.y + 3 * u * t2 * p2.y + t3 * p3.y,
};
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
export class InputSender {
private screenWidth: number;
private screenHeight: number;
constructor() {
this.screenWidth = GetSystemMetrics(SM_CXSCREEN);
this.screenHeight = GetSystemMetrics(SM_CYSCREEN);
}
async pressKey(vkCode: number): Promise<void> {
const scanCode = MapVirtualKeyW(vkCode, 0); // MAPVK_VK_TO_VSC
this.sendScanKeyDown(scanCode);
await randomDelay(30, 50);
this.sendScanKeyUp(scanCode);
await randomDelay(20, 40);
}
async keyDown(vkCode: number): Promise<void> {
const scanCode = MapVirtualKeyW(vkCode, 0);
this.sendScanKeyDown(scanCode);
await randomDelay(15, 30);
}
async keyUp(vkCode: number): Promise<void> {
const scanCode = MapVirtualKeyW(vkCode, 0);
this.sendScanKeyUp(scanCode);
await randomDelay(15, 30);
}
async typeText(text: string): Promise<void> {
for (const char of text) {
this.sendUnicodeChar(char);
await randomDelay(20, 50);
}
}
async paste(): Promise<void> {
await this.keyDown(VK.CONTROL);
await sleep(30);
await this.pressKey(VK.V);
await this.keyUp(VK.CONTROL);
await sleep(50);
}
async selectAll(): Promise<void> {
await this.keyDown(VK.CONTROL);
await sleep(30);
await this.pressKey(VK.A);
await this.keyUp(VK.CONTROL);
await sleep(50);
}
private getCursorPos(): Point {
const pt = { x: 0, y: 0 };
GetCursorPos(pt);
return pt;
}
private moveMouseRaw(x: number, y: number): void {
const normalizedX = Math.round((x * 65535) / this.screenWidth);
const normalizedY = Math.round((y * 65535) / this.screenHeight);
this.sendMouseInput(normalizedX, normalizedY, 0, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE);
}
async moveMouse(x: number, y: number): Promise<void> {
const start = this.getCursorPos();
const end: Point = { x, y };
const dx = end.x - start.x;
const dy = end.y - start.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Short distance: just teleport
if (distance < 10) {
this.moveMouseRaw(x, y);
await randomDelay(10, 20);
return;
}
// Generate 2 random control points offset from the straight line
const perpX = -dy / distance;
const perpY = dx / distance;
const spread = distance * 0.3;
const cp1: Point = {
x: start.x + dx * 0.25 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.25 + perpY * (Math.random() - 0.5) * spread,
};
const cp2: Point = {
x: start.x + dx * 0.75 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.75 + perpY * (Math.random() - 0.5) * spread,
};
const steps = clamp(Math.round(distance / 30), 8, 20);
for (let i = 1; i <= steps; i++) {
const rawT = i / steps;
const t = easeInOutQuad(rawT);
const pt = cubicBezier(t, start, cp1, cp2, end);
// Add ±1px jitter except on the last step
const jitterX = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0;
const jitterY = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0;
this.moveMouseRaw(Math.round(pt.x) + jitterX, Math.round(pt.y) + jitterY);
await sleep(1 + Math.random() * 2); // 1-3ms between steps
}
// Final exact landing
this.moveMouseRaw(x, y);
await randomDelay(5, 15);
}
moveMouseInstant(x: number, y: number): void {
this.moveMouseRaw(x, y);
}
/** Quick Bézier move — ~10-15ms, 5 steps, no jitter. Fast but not a raw teleport. */
async moveMouseFast(x: number, y: number): Promise<void> {
const start = this.getCursorPos();
const end: Point = { x, y };
const dx = end.x - start.x;
const dy = end.y - start.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) {
this.moveMouseRaw(x, y);
return;
}
const perpX = -dy / distance;
const perpY = dx / distance;
const spread = distance * 0.15;
const cp1: Point = {
x: start.x + dx * 0.3 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.3 + perpY * (Math.random() - 0.5) * spread,
};
const cp2: Point = {
x: start.x + dx * 0.7 + perpX * (Math.random() - 0.5) * spread,
y: start.y + dy * 0.7 + perpY * (Math.random() - 0.5) * spread,
};
const steps = 5;
for (let i = 1; i <= steps; i++) {
const t = easeInOutQuad(i / steps);
const pt = cubicBezier(t, start, cp1, cp2, end);
this.moveMouseRaw(Math.round(pt.x), Math.round(pt.y));
await sleep(2);
}
this.moveMouseRaw(x, y);
}
async leftClick(x: number, y: number): Promise<void> {
await this.moveMouse(x, y);
await randomDelay(20, 50);
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN);
await randomDelay(15, 40);
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTUP);
await randomDelay(15, 30);
}
async rightClick(x: number, y: number): Promise<void> {
await this.moveMouse(x, y);
await randomDelay(20, 50);
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN);
await randomDelay(15, 40);
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTUP);
await randomDelay(15, 30);
}
async ctrlRightClick(x: number, y: number): Promise<void> {
await this.keyDown(VK.CONTROL);
await randomDelay(30, 60);
await this.rightClick(x, y);
await this.keyUp(VK.CONTROL);
await randomDelay(30, 60);
}
async ctrlLeftClick(x: number, y: number): Promise<void> {
await this.keyDown(VK.CONTROL);
await randomDelay(30, 60);
await this.leftClick(x, y);
await this.keyUp(VK.CONTROL);
await randomDelay(30, 60);
}
private sendMouseInput(dx: number, dy: number, mouseData: number, flags: number): void {
const input = {
type: INPUT_MOUSE_TYPE,
_pad0: 0,
dx,
dy,
mouseData,
dwFlags: flags,
time: 0,
_pad1: 0,
dwExtraInfo: 0,
};
SendInput(1, koffi.as(input, 'INPUT_MOUSE *'), INPUT_SIZE);
}
private sendKeyInput(wVk: number, wScan: number, flags: number): void {
const input = {
type: INPUT_KEYBOARD_TYPE,
_pad0: 0,
wVk,
wScan,
dwFlags: flags,
time: 0,
_pad1: 0,
dwExtraInfo: 0,
_pad2: [0, 0, 0, 0, 0, 0, 0, 0],
};
SendInput(1, koffi.as(input, 'INPUT_KEYBOARD *'), INPUT_SIZE);
}
private sendScanKeyDown(scanCode: number): void {
this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE);
}
private sendScanKeyUp(scanCode: number): void {
this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP);
}
private sendUnicodeChar(char: string): void {
const code = char.charCodeAt(0);
this.sendKeyInput(0, code, KEYEVENTF_UNICODE);
this.sendKeyInput(0, code, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP);
}
}