283 lines
13 KiB
TypeScript
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;
|
|
}
|