Initial commit: POE2 automated trade bot
Monitors pathofexile.com/trade2 for new listings, travels to seller hideouts, buys items from public stash tabs, and stores them. Includes persistent C# OCR daemon for fast screen capture + Windows native OCR, web dashboard for managing trade links and settings, and full game automation via Win32 SendInput. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
41d174195e
28 changed files with 6449 additions and 0 deletions
294
src/game/InputSender.ts
Normal file
294
src/game/InputSender.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
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 / 15), 15, 40);
|
||||
|
||||
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(2 + Math.random() * 3); // 2-5ms between steps
|
||||
}
|
||||
|
||||
// Final exact landing
|
||||
this.moveMouseRaw(x, y);
|
||||
await randomDelay(10, 25);
|
||||
}
|
||||
|
||||
async leftClick(x: number, y: number): Promise<void> {
|
||||
await this.moveMouse(x, y);
|
||||
await randomDelay(50, 100);
|
||||
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN);
|
||||
await randomDelay(30, 80);
|
||||
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTUP);
|
||||
await randomDelay(30, 60);
|
||||
}
|
||||
|
||||
async rightClick(x: number, y: number): Promise<void> {
|
||||
await this.moveMouse(x, y);
|
||||
await randomDelay(50, 100);
|
||||
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN);
|
||||
await randomDelay(30, 80);
|
||||
this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTUP);
|
||||
await randomDelay(30, 60);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue