poe2-bot/src-old/server/routes/debug.ts
2026-02-13 01:12:11 -05:00

283 lines
13 KiB
TypeScript

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<OcrSettings>;
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;
}