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
129
src/game/ScreenReader.ts
Normal file
129
src/game/ScreenReader.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue