import { Router } from 'express'; import { mkdir } from 'fs/promises'; import { logger } from '../../util/logger.js'; import { sleep } from '../../util/sleep.js'; import { GRID_LAYOUTS } from '../../game/GridReader.js'; import type { Bot } from '../../bot/Bot.js'; import type { Server } from '../Server.js'; import type { OcrEngine, OcrPreprocess, DiffOcrParams, TooltipMethod, EdgeOcrParams } from '../../game/OcrDaemon.js'; import type { OcrSettings } from '../../game/ScreenReader.js'; export function debugRoutes(bot: Bot, server: Server): Router { const router = Router(); const notReady = (_req: any, res: any): boolean => { if (!bot.isReady) { res.status(503).json({ error: 'Not ready' }); return true; } return false; }; // --- Sync: OCR settings --- router.get('/ocr-settings', (req, res) => { if (notReady(req, res)) return; res.json({ ok: true, ...bot.screenReader.settings }); }); router.post('/ocr-settings', (req, res) => { if (notReady(req, res)) return; const body = req.body as Partial; const s = bot.screenReader.settings; if (body.engine && ['tesseract', 'easyocr', 'paddleocr'].includes(body.engine)) s.engine = body.engine; if (body.screenPreprocess && ['none', 'bgsub', 'tophat'].includes(body.screenPreprocess)) s.screenPreprocess = body.screenPreprocess; if (body.tooltipPreprocess && ['none', 'bgsub', 'tophat'].includes(body.tooltipPreprocess)) s.tooltipPreprocess = body.tooltipPreprocess; if (body.tooltipMethod && ['diff', 'edge'].includes(body.tooltipMethod)) s.tooltipMethod = body.tooltipMethod; if (body.tooltipParams != null) s.tooltipParams = body.tooltipParams; if (body.edgeParams != null) s.edgeParams = body.edgeParams; if (body.saveDebugImages != null) s.saveDebugImages = body.saveDebugImages; server.broadcastLog('info', `OCR settings updated: engine=${s.engine} screen=${s.screenPreprocess} tooltip=${s.tooltipPreprocess}`); res.json({ ok: true }); }); // --- Fire-and-forget: slow debug operations --- router.post('/screenshot', (req, res) => { if (notReady(req, res)) return; res.json({ ok: true }); bot.screenReader.saveDebugScreenshots('debug-screenshots').then(files => { server.broadcastLog('info', `Debug screenshots saved: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`); server.broadcastDebug('screenshot', { files }); }).catch(err => { logger.error({ err }, 'Debug screenshot failed'); server.broadcastDebug('screenshot', { error: 'Screenshot failed' }); }); }); router.post('/ocr', (req, res) => { if (notReady(req, res)) return; res.json({ ok: true }); bot.screenReader.readFullScreen().then(text => { server.broadcastLog('info', `OCR [${bot.screenReader.settings.engine}] (${text.length} chars): ${text.substring(0, 200)}`); server.broadcastDebug('ocr', { text }); }).catch(err => { logger.error({ err }, 'Debug OCR failed'); server.broadcastDebug('ocr', { error: 'OCR failed' }); }); }); router.post('/find-text', (req, res) => { if (notReady(req, res)) return; const { text } = req.body as { text: string }; if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } res.json({ ok: true }); bot.screenReader.findTextOnScreen(text).then(pos => { if (pos) { server.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) [${bot.screenReader.settings.engine}]`); } else { server.broadcastLog('warn', `"${text}" not found on screen [${bot.screenReader.settings.engine}]`); } server.broadcastDebug('find-text', { searchText: text, found: !!pos, position: pos }); }).catch(err => { logger.error({ err }, 'Debug find-text failed'); server.broadcastDebug('find-text', { searchText: text, error: 'Find text failed' }); }); }); router.post('/find-and-click', (req, res) => { if (notReady(req, res)) return; const { text, fuzzy } = req.body as { text: string; fuzzy?: boolean }; if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } res.json({ ok: true }); (async () => { const pos = await bot.screenReader.findTextOnScreen(text, !!fuzzy); if (pos) { await bot.gameController.focusGame(); await bot.gameController.leftClickAt(pos.x, pos.y); server.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) and clicked [${bot.screenReader.settings.engine}]`); server.broadcastDebug('find-and-click', { searchText: text, found: true, position: pos }); } else { server.broadcastLog('warn', `"${text}" not found on screen [${bot.screenReader.settings.engine}]`); server.broadcastDebug('find-and-click', { searchText: text, found: false, position: null }); } })().catch(err => { logger.error({ err }, 'Debug find-and-click failed'); server.broadcastDebug('find-and-click', { searchText: text, error: 'Find and click failed' }); }); }); router.post('/click', (req, res) => { if (notReady(req, res)) return; const { x, y } = req.body as { x: number; y: number }; if (x == null || y == null) { res.status(400).json({ error: 'Missing x/y' }); return; } res.json({ ok: true }); (async () => { await bot.gameController.focusGame(); await bot.gameController.leftClickAt(x, y); server.broadcastLog('info', `Clicked at (${x}, ${y})`); server.broadcastDebug('click', { x, y }); })().catch(err => { logger.error({ err }, 'Debug click failed'); server.broadcastDebug('click', { x, y, error: 'Click failed' }); }); }); router.post('/hideout', (req, res) => { if (notReady(req, res)) return; res.json({ ok: true }); (async () => { await bot.gameController.focusGame(); await bot.gameController.goToHideout(); server.broadcastLog('info', 'Sent /hideout command'); server.broadcastDebug('hideout', {}); })().catch(err => { logger.error({ err }, 'Debug hideout failed'); server.broadcastDebug('hideout', { error: 'Hideout command failed' }); }); }); router.post('/click-then-click', (req, res) => { if (notReady(req, res)) return; const { first, second, timeout = 5000 } = req.body as { first: string; second: string; timeout?: number }; if (!first || !second) { res.status(400).json({ error: 'Missing first/second' }); return; } res.json({ ok: true }); (async () => { const pos1 = await bot.screenReader.findTextOnScreen(first); if (!pos1) { server.broadcastLog('warn', `"${first}" not found on screen`); server.broadcastDebug('click-then-click', { first, second, found: false, step: 'first' }); return; } await bot.gameController.focusGame(); await bot.gameController.leftClickAt(pos1.x, pos1.y); server.broadcastLog('info', `Clicked "${first}" at (${pos1.x}, ${pos1.y}), waiting for "${second}"...`); const deadline = Date.now() + timeout; while (Date.now() < deadline) { const pos2 = await bot.screenReader.findTextOnScreen(second); if (pos2) { await bot.gameController.leftClickAt(pos2.x, pos2.y); server.broadcastLog('info', `Clicked "${second}" at (${pos2.x}, ${pos2.y})`); server.broadcastDebug('click-then-click', { first, second, found: true, position: pos2 }); return; } } server.broadcastLog('warn', `"${second}" not found after clicking "${first}" (timed out)`); server.broadcastDebug('click-then-click', { first, second, found: false, step: 'second' }); })().catch(err => { logger.error({ err }, 'Debug click-then-click failed'); server.broadcastDebug('click-then-click', { first, second, error: 'Click-then-click failed' }); }); }); router.post('/grid-scan', (req, res) => { if (notReady(req, res)) return; const { layout: layoutName, targetRow, targetCol } = req.body as { layout: string; targetRow?: number; targetCol?: number }; if (!GRID_LAYOUTS[layoutName]) { res.status(400).json({ error: `Unknown grid layout: ${layoutName}` }); return; } res.json({ ok: true }); (async () => { const result = await bot.screenReader.grid.scan(layoutName, undefined, targetRow, targetCol); const imageBuffer = await bot.screenReader.captureRegion(result.layout.region); const imageBase64 = imageBuffer.toString('base64'); const r = result.layout.region; const matchInfo = result.matches ? `, ${result.matches.length} matches` : ''; server.broadcastLog('info', `Grid scan (${layoutName}): ${result.layout.cols}x${result.layout.rows} at (${r.x},${r.y}) ${r.width}x${r.height} — ${result.occupied.length} occupied cells${matchInfo}`); server.broadcastDebug('grid-scan', { layout: layoutName, occupied: result.occupied, items: result.items, matches: result.matches, cols: result.layout.cols, rows: result.layout.rows, image: imageBase64, region: result.layout.region, targetRow, targetCol, }); })().catch(err => { logger.error({ err }, 'Debug grid-scan failed'); server.broadcastDebug('grid-scan', { layout: layoutName, error: 'Grid scan failed' }); }); }); router.post('/test-match-hover', (req, res) => { if (notReady(req, res)) return; const { layout: layoutName, targetRow, targetCol } = req.body as { layout: string; targetRow: number; targetCol: number }; if (!GRID_LAYOUTS[layoutName]) { res.status(400).json({ error: `Unknown layout: ${layoutName}` }); return; } if (targetRow == null || targetCol == null) { res.status(400).json({ error: 'Missing targetRow/targetCol' }); return; } res.json({ ok: true }); (async () => { server.broadcastLog('info', `Scanning ${layoutName} with target (${targetRow},${targetCol})...`); const result = await bot.screenReader.grid.scan(layoutName, undefined, targetRow, targetCol); const matches = result.matches ?? []; const items = result.items ?? []; const targetItem = items.find(i => targetRow >= i.row && targetRow < i.row + i.h && targetCol >= i.col && targetCol < i.col + i.w); const itemSize = targetItem ? `${targetItem.w}x${targetItem.h}` : '1x1'; server.broadcastLog('info', `Target (${targetRow},${targetCol}) is ${itemSize}, found ${matches.length} matches`); const hoverCells = [ { row: targetRow, col: targetCol, label: 'TARGET' }, ...matches.map(m => ({ row: m.row, col: m.col, label: `MATCH ${(m.similarity * 100).toFixed(0)}%` })), ]; await bot.gameController.focusGame(); const saveImages = bot.screenReader.settings.saveDebugImages; if (saveImages) await mkdir('items', { recursive: true }); const tooltips: Array<{ row: number; col: number; label: string; text: string }> = []; const ts = Date.now(); const reg = result.layout.region; const cellW = reg.width / result.layout.cols; const cellH = reg.height / result.layout.rows; // Move mouse to empty space and take reference snapshot bot.gameController.moveMouseInstant(reg.x + reg.width + 50, reg.y + reg.height / 2); await sleep(50); await bot.screenReader.snapshot(); if (saveImages) await bot.screenReader.saveScreenshot(`items/${ts}_snapshot.png`); await sleep(200); for (const cell of hoverCells) { const cellStart = performance.now(); const x = Math.round(reg.x + cell.col * cellW + cellW / 2); const y = Math.round(reg.y + cell.row * cellH + cellH / 2); await bot.gameController.moveMouseFast(x, y); await sleep(50); const afterMove = performance.now(); const imgPath = saveImages ? `items/${ts}_${cell.row}-${cell.col}.png` : undefined; const diff = await bot.screenReader.diffOcr(imgPath); const afterOcr = performance.now(); const text = diff.text.trim(); const regionInfo = diff.region ? ` at (${diff.region.x},${diff.region.y}) ${diff.region.width}x${diff.region.height}` : ''; tooltips.push({ row: cell.row, col: cell.col, label: cell.label, text }); server.broadcastLog('info', `${cell.label} (${cell.row},${cell.col}) [move: ${(afterMove - cellStart).toFixed(0)}ms, ocr: ${(afterOcr - afterMove).toFixed(0)}ms, total: ${(afterOcr - cellStart).toFixed(0)}ms]${regionInfo}:`); if (diff.lines.length > 0) { for (const line of diff.lines) { server.broadcastLog('info', ` ${line.text}`); } } else if (text) { for (const line of text.split('\n')) { if (line.trim()) server.broadcastLog('info', ` ${line.trim()}`); } } } server.broadcastLog('info', `Done — hovered ${hoverCells.length} cells, read ${tooltips.filter(t => t.text).length} tooltips`); server.broadcastDebug('test-match-hover', { itemSize, matchCount: matches.length, hoveredCount: hoverCells.length, tooltips, }); })().catch(err => { logger.error({ err }, 'Debug test-match-hover failed'); server.broadcastDebug('test-match-hover', { error: 'Test match hover failed' }); }); }); return router; }