import { automationBlockReason, requiresAdminForAutomation } from "../../../lib/automationPlanner"; import { captureRejectionReason } from "../../../lib/scannerCaptureQuality"; import { runAutoScanLoop } from "../../../lib/autoScanLoop"; import { clampScanLimit, emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession"; import { getAutoReviewReason, wait } from "../../../lib/scanReviewUtils"; import type { AutomationRepositoryPort, RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositories"; import type { AutomationGuard, BooleanResult, CaptureOptions, CaptureResult, ClickResult, FocusGenshinResult, RuntimeInfo, ScrollResult, } from "../../../types/global"; import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser"; import type { MutableRefObject } from "react"; import type { Dispatch, SetStateAction } from "react"; export interface ScanActionContext { autoScanRunning: boolean; setAutoScanRunning: Dispatch>; isScanning: boolean; stopVisibleScanRef: MutableRefObject; selectedSourceId: string; bridgeReady: boolean; automationRepo?: AutomationRepositoryPort; runtimeRepo?: RuntimeRepositoryPort; runtimeInfo?: RuntimeInfo | null; scanLimit: number; skipRows: number; detectedInventoryCount: number; setScanSummary: Dispatch>; setAutoScanStats: Dispatch>; setReviewStatus: Dispatch>; appendAutomationLog: (line: string) => void; appendClickDiagnostics: (result: ClickResult, prefix?: string) => void; captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise; captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise; parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null; persistParsedArtifact: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate, source: string, needsReview: boolean) => Promise; saveReviewSample: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason?: string) => Promise; shouldFlagArtifactForReview: (parsed: ParsedArtifactCandidate | null) => boolean; focusDashboard: () => Promise; } function buildScanSignature(parsed: ParsedArtifactCandidate) { return `${parsed.name}-${parsed.slot}-${parsed.mainStat}-${parsed.mainValue}-${parsed.level ?? 0}`; } export async function runAutoReviewScan(context: ScanActionContext): Promise { const { autoScanRunning, selectedSourceId, stopVisibleScanRef, setAutoScanRunning, setReviewStatus, setScanSummary, setAutoScanStats, appendAutomationLog, scanLimit, detectedInventoryCount, captureSelectedSource, parseArtifact, persistParsedArtifact, saveReviewSample, shouldFlagArtifactForReview, focusDashboard, } = context; if (autoScanRunning || !selectedSourceId) return; setAutoScanRunning(true); stopVisibleScanRef.current = false; setScanSummary(null); setAutoScanStats(emptyAutoScanStats); setReviewStatus("Manueller Scan laeuft. Klicke in Genshin auf ein anderes Artifact; nur neue Artifacts werden verarbeitet."); const seen = new Set(); const stats: AutoScanStats = { ...emptyAutoScanStats, pages: 1 }; let idleTicks = 0; const maxArtifacts = resolveScanTargetCount(scanLimit, detectedInventoryCount); const maxIdleTicks = 90; while (!stopVisibleScanRef.current && stats.parsed < maxArtifacts && idleTicks < maxIdleTicks) { const capture = await captureSelectedSource(0, true); const rejection = captureRejectionReason(capture, parseArtifact(capture)); const parsed = parseArtifact(capture); if (!capture || !parsed || rejection) { if (capture && rejection) { await saveReviewSample(capture, parsed, `manual:capture-rejected`); stats.review++; setAutoScanStats({ ...stats }); } idleTicks++; setReviewStatus(`Manueller Scan wartet auf ein lesbares Artifact... (${stats.parsed}/${maxArtifacts})${rejection ? ` ${rejection}` : ""}`); await wait(700); continue; } const signature = buildScanSignature(parsed); if (seen.has(signature)) { idleTicks++; setReviewStatus(`Manueller Scan wartet auf ein neues Artifact... (${stats.parsed}/${maxArtifacts})`); await wait(700); continue; } seen.add(signature); idleTicks = 0; stats.attempted++; stats.verified++; stats.parsed++; const reason = getAutoReviewReason(capture, parsed); const needsReview = shouldFlagArtifactForReview(parsed); if (reason) { await saveReviewSample(capture, parsed, `manual:${reason}`); stats.review++; } if (await persistParsedArtifact(capture, parsed, "manual-scan", needsReview)) { stats.stored++; } setAutoScanStats({ ...stats }); setReviewStatus(`Manueller Scan: neues Artifact erkannt (${stats.parsed}/${maxArtifacts}). Klicke das naechste Artifact an oder druecke Stop.`); await wait(700); } setAutoScanRunning(false); const status: ScanSummary["status"] = stopVisibleScanRef.current ? "stopped" : "done"; const idleSuffix = idleTicks >= maxIdleTicks ? " Keine neuen Artifacts erkannt; manueller Scan beendet." : ""; await focusDashboard(); setReviewStatus( `Manueller Scan ${status}. ${stats.verified} neue Ansichten verifiziert, ${stats.parsed} Artifacts gelesen, ${stats.stored} in der Datenbank gespeichert, ${stats.review} Review-Samples.${idleSuffix}`, ); setScanSummary({ mode: "Manueller Scan", status, ...stats, targetCount: maxArtifacts, gridLabel: "Nur neue, vom User angeklickte Artifacts wurden verarbeitet.", }); appendAutomationLog(`manual scan finished: ${stats.parsed} parsed, ${stats.stored} stored, ${stats.review} review`); } export async function runVisibleGridScan(context: ScanActionContext): Promise { const { autoScanRunning, bridgeReady, selectedSourceId, runtimeInfo, automationRepo, runtimeRepo, stopVisibleScanRef, setAutoScanRunning, setScanSummary, setAutoScanStats, setReviewStatus, appendAutomationLog, appendClickDiagnostics, captureSelectedSource, captureFastSelectedSource, parseArtifact, persistParsedArtifact, saveReviewSample, shouldFlagArtifactForReview, scanLimit, skipRows, detectedInventoryCount, focusDashboard, } = context; if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.focusGenshinForScanStart || !automationRepo?.focusGenshin || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) return; const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo); if (requiresAdminForAutoScan) { setReviewStatus( "App laeuft nicht als Administrator. Bitte die App schliessen und als Administrator neu starten - Auto-Scan braucht Administrator-Rechte, damit Windows die simulierten Eingaben an Genshin nicht blockiert.", ); setScanSummary({ mode: "Automatischer Scan", status: "blocked", ...emptyAutoScanStats, targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount), gridLabel: "App laeuft nicht als Administrator. Neustart als Administrator noetig.", }); return; } setAutoScanRunning(true); stopVisibleScanRef.current = false; setScanSummary(null); setAutoScanStats(emptyAutoScanStats); setReviewStatus("Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort."); const freshRuntime = await runtimeRepo?.getRuntimeInfo().catch(() => null); const adminBlockReason = automationBlockReason(freshRuntime); if (adminBlockReason) { setAutoScanRunning(false); setReviewStatus(adminBlockReason); appendAutomationLog("blocked: App laeuft nicht als Administrator, keine In-Game-Klicks ausgefuehrt"); setScanSummary({ mode: "Automatischer Scan", status: "blocked", ...emptyAutoScanStats, targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount), gridLabel: adminBlockReason, }); return; } if (freshRuntime) { const required = freshRuntime.genshinFound ? `found:${freshRuntime.targetProcess || "genshin"}` : "not-found"; appendAutomationLog(`runtime ping: elevated=${freshRuntime.isElevated} ${required}`); } const focusGenshinForScanStart = automationRepo.focusGenshinForScanStart ?? automationRepo.focusGenshin; setReviewStatus("Genshin wird in den Vordergrund geholt..."); let focusResult: FocusGenshinResult | null = null; for (let attempt = 1; attempt <= 3; attempt += 1) { const current = await focusGenshinForScanStart().catch(() => null); if (!current) { appendAutomationLog(`focus attempt ${attempt}/3: exception`); } else { focusResult = current; appendAutomationLog( `focus attempt ${attempt}/3: ${current.focused ? "ok" : "failed"} found:${current.genshinFound ? "yes" : "no"} setForeground:${current.setForegroundResult ?? "n/a"} target:${current.targetProcess || "?"} fg:${current.foregroundProcess || "?"}`, ); if (current.focused) break; if (!current.genshinFound) break; } if (attempt < 3) await wait(300); } if (!focusResult?.focused) { setAutoScanRunning(false); const reason = !focusResult?.genshinFound ? "Genshin-Prozess wurde nicht gefunden. Bitte pruefen, ob Genshin laeuft, und Auto-Scan erneut starten." : "Genshin konnte nicht in den Vordergrund geholt werden. Bitte Genshin manuell anklicken/fokussieren und Auto-Scan erneut starten."; setReviewStatus(reason); setScanSummary({ mode: "Automatischer Scan", status: "blocked", ...emptyAutoScanStats, targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount), gridLabel: reason, }); return; } setReviewStatus("Genshin ist im Vordergrund. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort."); const result = await runAutoScanLoop( { api: { clickScreen: (x: number, y: number) => { if (!automationRepo?.clickScreen) { return Promise.resolve({ ok: false, x: 0, y: 0, clicked: false, moved: false, focused: false, inputBlocked: false, }); } return automationRepo.clickScreen(x, y); }, scrollScreen: (notches: number, anchorX?: number, anchorY?: number) => { if (!automationRepo?.scrollScreen) { return Promise.resolve({ ok: false, notchesSent: 0, inputBlocked: false }); } return automationRepo.scrollScreen(notches, anchorX, anchorY); }, getAutomationGuard: () => automationRepo?.getAutomationGuard?.() ?? Promise.resolve({ ok: false, escapePressed: false, enterPressed: false, f9Pressed: false }), }, captureSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin), captureFastSelectedSource, parseArtifact, persistParsedArtifact, saveReviewSample, getAutoReviewReason, shouldFlagArtifactForReview, appendAutomationLog, appendClickDiagnostics, setReviewStatus, setAutoScanStats, shouldStop: () => stopVisibleScanRef.current, }, { scanLimit, skipRows, detectedInventoryCount, }, ); setAutoScanRunning(false); if (result.blockedReason) appendAutomationLog(`stop: ${result.blockedReason}`); await focusDashboard(); setReviewStatus(`Automatischer Scan ${result.status === "stopped" ? "gestoppt" : result.status === "blocked" ? "blockiert" : "fertig"}. ${result.stats.clicked} Klicks, ${result.stats.attempted} Positionen bearbeitet, ${result.stats.verified} Ansichten verifiziert, ${result.stats.parsed} gelesen, ${result.stats.stored} in der Datenbank, ${result.stats.review} Review-Samples, ${result.stats.duplicates} Duplikate, ${result.stats.misses} Misses.${result.blockedReason ? ` ${result.blockedReason}` : ""}`); setScanSummary({ mode: "Automatischer Scan", status: result.status, ...result.stats, targetCount: result.targetCount, gridLabel: result.gridLabel, }); }