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:
Boki 2026-02-10 14:03:47 -05:00
commit 41d174195e
28 changed files with 6449 additions and 0 deletions

129
src/game/ScreenReader.ts Normal file
View file

@ -0,0 +1,129 @@
import { mkdir } from 'fs/promises';
import { join } from 'path';
import { logger } from '../util/logger.js';
import { OcrDaemon, type OcrResponse } from './OcrDaemon.js';
import type { Region } from '../types.js';
function elapsed(start: number): string {
return `${(performance.now() - start).toFixed(0)}ms`;
}
export class ScreenReader {
private daemon = new OcrDaemon();
// ── Screenshot capture ──────────────────────────────────────────────
async captureScreen(): Promise<Buffer> {
const t = performance.now();
const buf = await this.daemon.captureBuffer();
logger.info({ ms: elapsed(t) }, 'captureScreen');
return buf;
}
async captureRegion(region: Region): Promise<Buffer> {
const t = performance.now();
const buf = await this.daemon.captureBuffer(region);
logger.info({ ms: elapsed(t) }, 'captureRegion');
return buf;
}
// ── OCR helpers ─────────────────────────────────────────────────────
private findWordInOcrResult(
result: OcrResponse,
needle: string,
): { x: number; y: number } | null {
const lower = needle.toLowerCase();
for (const line of result.lines) {
for (const word of line.words) {
if (word.text.toLowerCase().includes(lower)) {
return {
x: Math.round(word.x + word.width / 2),
y: Math.round(word.y + word.height / 2),
};
}
}
}
return null;
}
// ── Full-screen methods ─────────────────────────────────────────────
async findTextOnScreen(
searchText: string,
): Promise<{ x: number; y: number } | null> {
const t = performance.now();
const result = await this.daemon.ocr();
const pos = this.findWordInOcrResult(result, searchText);
if (pos) {
logger.info({ searchText, x: pos.x, y: pos.y, totalMs: elapsed(t) }, 'Found text on screen');
} else {
logger.info({ searchText, totalMs: elapsed(t) }, 'Text not found on screen');
}
return pos;
}
async readFullScreen(): Promise<string> {
const result = await this.daemon.ocr();
return result.text;
}
// ── Region methods ──────────────────────────────────────────────────
async findTextInRegion(
region: Region,
searchText: string,
): Promise<{ x: number; y: number } | null> {
const t = performance.now();
const result = await this.daemon.ocr(region);
const pos = this.findWordInOcrResult(result, searchText);
if (pos) {
// Offset back to screen space
const screenPos = { x: region.x + pos.x, y: region.y + pos.y };
logger.info({ searchText, x: screenPos.x, y: screenPos.y, region, totalMs: elapsed(t) }, 'Found text in region');
return screenPos;
}
logger.info({ searchText, region, totalMs: elapsed(t) }, 'Text not found in region');
return null;
}
async readRegionText(region: Region): Promise<string> {
const result = await this.daemon.ocr(region);
return result.text;
}
async checkForText(region: Region, searchText: string): Promise<boolean> {
const pos = await this.findTextInRegion(region, searchText);
return pos !== null;
}
// ── Save utilities ──────────────────────────────────────────────────
async saveScreenshot(path: string): Promise<void> {
await this.daemon.saveScreenshot(path);
logger.info({ path }, 'Screenshot saved');
}
async saveDebugScreenshots(dir: string): Promise<string[]> {
await mkdir(dir, { recursive: true });
const ts = Date.now();
const originalPath = join(dir, `${ts}-screenshot.png`);
await this.daemon.saveScreenshot(originalPath);
logger.info({ dir, files: [originalPath.split(/[\\/]/).pop()] }, 'Debug screenshot saved');
return [originalPath];
}
async saveRegion(region: Region, path: string): Promise<void> {
await this.daemon.saveScreenshot(path, region);
logger.info({ path, region }, 'Region screenshot saved');
}
// ── Lifecycle ───────────────────────────────────────────────────────
async dispose(): Promise<void> {
await this.daemon.stop();
}
}