diff --git a/src/bot/Bot.ts b/src/bot/Bot.ts index ff012f7..fb886bc 100644 --- a/src/bot/Bot.ts +++ b/src/bot/Bot.ts @@ -214,24 +214,39 @@ export class Bot extends EventEmitter { this.gameController, this.screenReader, this.logWatcher, this.config, ); - // /hideout + waitForAreaTransition - this.emit('log', 'info', 'Sending /hideout command...'); - await this.gameController.focusGame(); - const arrivedHome = await this.inventoryManager.waitForAreaTransition( - this.config.travelTimeoutMs, - () => this.gameController.goToHideout(), - ); - if (arrivedHome) { + // Pre-warm OCR daemon + EasyOCR model in background (don't await yet) + const ocrWarmup = this.screenReader.warmup().catch(err => { + logger.warn({ err }, 'OCR warmup failed (will retry on first use)'); + }); + + // Check if already in hideout from log tail + const alreadyInHideout = this.logWatcher.currentArea.toLowerCase().includes('hideout'); + + if (alreadyInHideout) { + logger.info({ area: this.logWatcher.currentArea }, 'Already in hideout, skipping /hideout command'); this.inventoryManager.setLocation(true); - this.logWatcher.currentArea = 'Hideout'; } else { - this.inventoryManager.setLocation(true); - this.logWatcher.currentArea = 'Hideout'; - logger.warn('Timed out waiting for hideout transition on startup (may already be in hideout)'); + this.emit('log', 'info', 'Sending /hideout command...'); + await this.gameController.focusGame(); + const arrivedHome = await this.inventoryManager.waitForAreaTransition( + this.config.travelTimeoutMs, + () => this.gameController.goToHideout(), + ); + if (arrivedHome) { + this.inventoryManager.setLocation(true); + this.logWatcher.currentArea = 'Hideout'; + } else { + this.inventoryManager.setLocation(true); + this.logWatcher.currentArea = 'Hideout'; + logger.warn('Timed out waiting for hideout transition on startup (may already be in hideout)'); + } } this.state = 'IN_HIDEOUT'; this.emit('log', 'info', 'In hideout, ready to trade'); + // Ensure OCR warmup finished before proceeding to inventory scan + await ocrWarmup; + // Clear leftover inventory this.emit('log', 'info', 'Checking inventory for leftover items...'); await this.inventoryManager.clearToStash(); diff --git a/src/game/OcrDaemon.ts b/src/game/OcrDaemon.ts index 936e3d4..396bf1e 100644 --- a/src/game/OcrDaemon.ts +++ b/src/game/OcrDaemon.ts @@ -71,16 +71,37 @@ export type OcrEngine = 'tesseract' | 'easyocr' | 'paddleocr'; export type OcrPreprocess = 'none' | 'bgsub' | 'tophat'; -export interface DiffOcrParams { +export interface DiffCropParams { diffThresh?: number; + rowThreshDiv?: number; + colThreshDiv?: number; maxGap?: number; trimCutoff?: number; + ocrPad?: number; +} + +export interface OcrParams { kernelSize?: number; upscale?: number; + useBackgroundSub?: boolean; dimPercentile?: number; textThresh?: number; softThreshold?: boolean; - useBackgroundSub?: boolean; + usePerLineOcr?: boolean; + lineGapTolerance?: number; + linePadY?: number; + psm?: number; + mergeGap?: number; + linkThreshold?: number; + textThreshold?: number; + lowText?: number; + widthThs?: number; + paragraph?: boolean; +} + +export interface DiffOcrParams { + crop?: DiffCropParams; + ocr?: OcrParams; } interface DaemonRequest { @@ -236,6 +257,11 @@ export class OcrDaemon { }; } + /** Eagerly spawn the daemon process so it's ready for the first real request. */ + async warmup(): Promise { + await this.ensureRunning(); + } + async stop(): Promise { this.stopped = true; if (this.proc) { diff --git a/src/game/ScreenReader.ts b/src/game/ScreenReader.ts index 0eb643b..f2bd8dd 100644 --- a/src/game/ScreenReader.ts +++ b/src/game/ScreenReader.ts @@ -1,7 +1,7 @@ import { mkdir } from 'fs/promises'; import { join } from 'path'; import { logger } from '../util/logger.js'; -import { OcrDaemon, type OcrResponse, type OcrEngine, type OcrPreprocess, type DiffOcrParams, type DiffOcrResponse, type TemplateMatchResult } from './OcrDaemon.js'; +import { OcrDaemon, type OcrResponse, type OcrEngine, type OcrPreprocess, type DiffOcrParams, type DiffCropParams, type OcrParams, type DiffOcrResponse, type TemplateMatchResult } from './OcrDaemon.js'; import { GridReader, type GridLayout, type CellCoord } from './GridReader.js'; import type { Region } from '../types.js'; @@ -23,11 +23,28 @@ export class ScreenReader { settings: OcrSettings = { engine: 'easyocr', screenPreprocess: 'none', - tooltipPreprocess: 'bgsub', - tooltipParams: {}, + tooltipPreprocess: 'tophat', + tooltipParams: { + crop: { diffThresh: 10 }, + ocr: { kernelSize: 21 }, + }, saveDebugImages: true, }; + /** + * Eagerly spawn the OCR daemon and warm up the EasyOCR model. + * Fire-and-forget a small OCR request so the Python model loads in the background. + */ + async warmup(): Promise { + await this.daemon.warmup(); + // Fire a small EasyOCR request to trigger Python model load + // Use a tiny 1×1 region to minimize work, we only care about loading the model + const { engine } = this.settings; + if (engine !== 'tesseract') { + await this.daemon.ocr({ x: 0, y: 0, width: 100, height: 100 }, engine); + } + } + // ── Screenshot capture ────────────────────────────────────────────── async captureScreen(): Promise { diff --git a/src/inventory/InventoryTracker.ts b/src/inventory/InventoryTracker.ts index 87a3bb8..a46b494 100644 --- a/src/inventory/InventoryTracker.ts +++ b/src/inventory/InventoryTracker.ts @@ -39,8 +39,12 @@ export class InventoryTracker { } } - // Record detected items + // Record detected items, filtering out impossibly large ones (max POE2 item = 2×4) for (const item of items) { + if (item.w > 2 || item.h > 4) { + logger.warn({ row: item.row, col: item.col, w: item.w, h: item.h }, 'Ignoring oversized item (false positive)'); + continue; + } this.items.push({ row: item.row, col: item.col, w: item.w, h: item.h, postAction: defaultAction }); } diff --git a/src/log/ClientLogWatcher.ts b/src/log/ClientLogWatcher.ts index 8148dac..6ebd5e3 100644 --- a/src/log/ClientLogWatcher.ts +++ b/src/log/ClientLogWatcher.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import { watch } from 'chokidar'; -import { createReadStream, statSync } from 'fs'; +import { createReadStream, statSync, openSync, readSync, closeSync } from 'fs'; import { createInterface } from 'readline'; import { logger } from '../util/logger.js'; @@ -32,6 +32,8 @@ export class ClientLogWatcher extends EventEmitter { try { const stats = statSync(this.logPath); this.fileOffset = stats.size; + // Read tail of log to determine current area before we start watching + this.detectCurrentArea(stats.size); } catch { logger.warn({ path: this.logPath }, 'Log file not found yet, will watch for creation'); this.fileOffset = 0; @@ -47,7 +49,39 @@ export class ClientLogWatcher extends EventEmitter { this.readNewLines(); }); - logger.info({ path: this.logPath }, 'Watching Client.txt for game events'); + logger.info({ path: this.logPath, currentArea: this.currentArea || '(unknown)' }, 'Watching Client.txt for game events'); + } + + /** Read the last chunk of the log file to determine the current area. */ + private detectCurrentArea(fileSize: number): void { + const TAIL_BYTES = 8192; + const start = Math.max(0, fileSize - TAIL_BYTES); + const buf = Buffer.alloc(Math.min(TAIL_BYTES, fileSize)); + const fd = openSync(this.logPath, 'r'); + try { + readSync(fd, buf, 0, buf.length, start); + } finally { + closeSync(fd); + } + const tail = buf.toString('utf-8'); + const lines = tail.split(/\r?\n/); + + // Walk backwards to find the most recent area transition + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + const sceneMatch = line.match(/\[SCENE\] Set Source \[(.+?)\]/); + if (sceneMatch && sceneMatch[1] !== '(null)') { + this.currentArea = sceneMatch[1]; + logger.info({ area: this.currentArea }, 'Detected current area from log tail'); + return; + } + const areaMatch = line.match(/You have entered (.+?)\.?$/); + if (areaMatch) { + this.currentArea = areaMatch[1]; + logger.info({ area: this.currentArea }, 'Detected current area from log tail'); + return; + } + } } private readNewLines(): void { diff --git a/src/server/index.html b/src/server/index.html index c6a9afb..af798f3 100644 --- a/src/server/index.html +++ b/src/server/index.html @@ -558,7 +558,7 @@
Engine
- @@ -582,12 +582,52 @@
-
Tooltip OCR
+
Tooltip Preprocess
+
+ +
+
Crop Detection
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
OCR Processing
+
+
+ + +
+
+ + +
+
+