// 全局状态机配置对象 let stateMachineConfig = null; // 全局游戏画面区域 let gameRegion = null; // 主逻辑 (async function () { // 只有当未开启截图模式时,才检查 verifyUid 和 targetUid if (!settings.screenshotMode && settings.verifyUid && !settings.targetUid) { const msg = '已启用UID校验,但未填写目标UID,请检查配置'; log.error(msg); notification.error(msg); return; } // 读取状态配置文件 let statesData; try { const statesJsonText = file.readTextSync('assets/states.json'); statesData = JSON.parse(statesJsonText); } catch (e) { log.error(`读取 states.json 失败:${e.message},状态机无法正常工作`); throw e; } if (!statesData || !Array.isArray(statesData)) { const errorMsg = 'states.json 格式错误:应为状态对象数组'; log.error(errorMsg); throw new Error(errorMsg); } // 将状态数组转换为对象,以状态名称为key stateMachineConfig = {}; for (const state of statesData) { stateMachineConfig[state.name] = state; } log.info(`成功加载 ${statesData.length} 个状态配置`); // 检查是否进入截图模式 if (settings.screenshotMode) { await handleScreenshotMode(); return; } // 检查是否需要UID校验 if (settings.verifyUid) { log.info('开始:验证当前账号UID'); const currentUid = await verifyCurrentUid(); if (isUidMatch(currentUid, settings.targetUid)) { log.info(`当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,无需切换`); notification.Send(`当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,无需切换`); return; } } // 根据是否开启UID校验决定最大尝试次数 let attempts = 0; const maxAttempts = settings.verifyUid ? 2 : 1; let switchSuccess = false; let notificationMessage = null; while (attempts < maxAttempts) { attempts++; log.info(`开始第 ${attempts} 次账号切换尝试`); // 用于保存通知消息 let currentNotificationMessage = null; let currentSwitchSuccess = false; // 判断是否跳过搜索,直接使用账号密码 let useAccountPassword = settings.skipSearch; if (!useAccountPassword) { // 尝试预加载账号图片模板 let accountImageMat = null; const accountImagePath = `accounts/${settings.targetUid}.png`; try { accountImageMat = file.ReadImageMatSync(accountImagePath); log.info(`成功预加载账号图片:${accountImagePath}`); } catch (e) { log.warn(`预加载账号图片失败:${accountImagePath},错误:${e.message}`); log.warn('将使用账号密码方式登录'); useAccountPassword = true; } // 如果图片预加载成功,尝试使用图片查找方式 if (!useAccountPassword) { // 尝试查找账号图片分支 log.info('开始:尝试使用账号图片查找方式'); // 首先定位到"进入游戏或登录其他账号"状态 log.info('开始:尝试切换到 enterGame 状态'); const enterGameResult = await goToState('enterGame'); if (!enterGameResult) { log.warn('失败:未能到达 enterGame 状态,将尝试使用账号密码方式'); useAccountPassword = true; } else { log.info('成功到达 enterGame 状态'); // 检查当前界面是否存在目标账号图片(适用于只有一个账号的情况) log.info(`尝试在 enterGame 状态查找账号图片:${accountImagePath}`); const accountRo = RecognitionObject.TemplateMatch(accountImageMat, 0, 0, 1920, 1080); accountRo.Threshold = parseFloat(settings.accountImageThreshold) || 0.9; accountRo.InitTemplate(); const uidFoundInEnterGame = await findAndClick(accountRo, false, 1000); if (uidFoundInEnterGame) { log.info(`在 enterGame 状态找到账号图片:${settings.targetUid}.png`); // 点击进入游戏按钮 await findAndClick('assets/RecognitionObjects/EnterGame.png', true, 1000); // 定位到主界面 log.info('开始:尝试切换到 mainUI 状态'); const mainUIResult = await goToState('mainUI'); if (mainUIResult) { log.info('成功到达 mainUI 状态,账号切换完成'); currentSwitchSuccess = true; currentNotificationMessage = `使用账号图片方式成功切换到uid为${settings.targetUid}的账号`; } else { log.warn('失败:未能到达 mainUI 状态,将尝试使用账号密码方式'); useAccountPassword = true; } } else { log.info('在 enterGame 状态未找到账号图片,尝试展开账号列表'); // 定位到选择账号界面 log.info('开始:尝试切换到 selectAccount 状态'); const selectAccountResult = await goToState('selectAccount'); if (!selectAccountResult) { log.warn('失败:未能到达 selectAccount 状态,将尝试使用账号密码方式'); useAccountPassword = true; } else { log.info('成功到达 selectAccount 状态'); // 使用预加载的图片模板进行查找 log.info(`尝试查找并点击账号图片:${accountImagePath}`); const accountRo = RecognitionObject.TemplateMatch(accountImageMat, 0, 0, 1920, 1080); accountRo.Threshold = parseFloat(settings.accountImageThreshold) || 0.9; accountRo.InitTemplate(); const uidFound = await findAndClick(accountRo, true, 5000); if (uidFound) { log.info(`成功点击账号图片:${settings.targetUid}.png`); // 定位到主界面 log.info('开始:尝试切换到 mainUI 状态'); const mainUIResult = await goToState('mainUI'); if (mainUIResult) { log.info('成功到达 mainUI 状态,账号切换完成'); currentSwitchSuccess = true; currentNotificationMessage = `使用账号图片方式成功切换到uid为${settings.targetUid}的账号`; } else { log.warn('失败:未能到达 mainUI 状态,将尝试使用账号密码方式'); useAccountPassword = true; } } else { log.warn(`未找到账号图片:${settings.targetUid}.png,将尝试使用账号密码方式`); useAccountPassword = true; } } } } } } else { log.info('已勾选跳过搜索,直接使用账号密码登录'); } // 账号密码分支 if (useAccountPassword) { log.info('开始:使用账号密码登录方式'); // 检查账号密码是否为空 if (!settings.account || !settings.password) { log.error('账号或密码为空,无法使用账号密码方式登录'); log.error(`账号:${settings.account ? '已设置' : '未设置'},密码:${settings.password ? '已设置' : '未设置'}`); currentNotificationMessage = '切换失败,账号或密码为空'; } else { // 切换到"输入账号密码"状态 log.info('开始:尝试切换到 enterAccountAndPassword 状态'); const result = await goToState('enterAccountAndPassword'); if (!result) { log.warn('失败:未能到达 enterAccountAndPassword 状态'); currentNotificationMessage = '切换失败,未能到达输入账号密码界面'; } else { log.info('成功到达 enterAccountAndPassword 状态'); // 循环点击同意按钮,直到找不到为止 log.info('开始循环点击同意按钮'); let agreeFound = true; while (agreeFound) { // 创建带阈值的识别对象 const agreeRo = RecognitionObject.TemplateMatch( file.ReadImageMatSync('assets/RecognitionObjects/Agree.png'), 0, 0, 1920, 1080 ); agreeRo.Threshold = 0.9; agreeFound = await findAndClick(agreeRo, true, 1000); if (agreeFound) { log.info('点击了同意按钮,继续查找'); await sleep(500); } else { log.info('未找到同意按钮,循环结束'); } } // 输入账号 log.info('开始输入账号'); let accountInputFound = true; while (accountInputFound) { accountInputFound = await findAndClick('assets/RecognitionObjects/EnterAccount.png', true, 1000); if (accountInputFound) { log.info('点击了账号输入框'); await sleep(100); // 输入账号 inputText(settings.account); log.info('已输入账号'); await sleep(500); // 检查输入框是否还存在(验证输入是否成功) const checkRo = RecognitionObject.TemplateMatch( file.ReadImageMatSync('assets/RecognitionObjects/EnterAccount.png'), 0, 0, 1920, 1080 ); let tempRegion = null; try { tempRegion = captureGameRegion(); accountInputFound = tempRegion.find(checkRo).isExist(); } catch (e) { log.error(`检查账号输入框时出错:${e.message}`); accountInputFound = false; } finally { if (tempRegion) { tempRegion.dispose(); } } if (accountInputFound) { log.warn('账号输入框仍然存在,可能输入失败,重试'); } else { log.info('账号输入完成'); } } else { log.warn('未找到账号输入框'); } } // 输入密码 log.info('开始输入密码'); let passwordInputFound = true; while (passwordInputFound) { passwordInputFound = await findAndClick('assets/RecognitionObjects/EnterPassword.png', true, 1000); if (passwordInputFound) { log.info('点击了密码输入框'); await sleep(100); // 输入密码 inputText(settings.password); log.info('已输入密码'); await sleep(500); // 检查输入框是否还存在(验证输入是否成功) const checkRo = RecognitionObject.TemplateMatch( file.ReadImageMatSync('assets/RecognitionObjects/EnterPassword.png'), 0, 0, 1920, 1080 ); let tempRegion = null; try { tempRegion = captureGameRegion(); passwordInputFound = tempRegion.find(checkRo).isExist(); } catch (e) { log.error(`检查密码输入框时出错:${e.message}`); passwordInputFound = false; } finally { if (tempRegion) { tempRegion.dispose(); } } if (passwordInputFound) { log.warn('密码输入框仍然存在,可能输入失败,重试'); } else { log.info('密码输入完成'); } } else { log.warn('未找到密码输入框'); } } // 点击进入游戏按钮,直到消失 log.info('开始点击进入游戏按钮'); let enterGame2Found = true; while (enterGame2Found) { enterGame2Found = await findAndClick('assets/RecognitionObjects/EnterGame2.png', true, 1000); if (enterGame2Found) { log.info('点击了进入游戏按钮,继续查找'); await sleep(500); } else { log.info('未找到进入游戏按钮,循环结束'); } } // 切换到主界面 log.info('开始:尝试切换到 mainUI 状态'); const mainUIResult = await goToState('mainUI'); if (mainUIResult) { log.info('成功到达 mainUI 状态,账号切换完成'); currentSwitchSuccess = true; currentNotificationMessage = `使用账号密码方式成功切换到uid为${settings.targetUid}的账号`; } else { log.warn('失败:未能到达 mainUI 状态'); currentNotificationMessage = '切换失败,未能到达主界面'; } } } } // 统一处理:回到主界面 if (!currentSwitchSuccess) { log.info('尝试返回主界面'); const backToMainResult = await goToState('mainUI'); if (backToMainResult) { log.info('已返回主界面'); } else { log.warn('未能返回主界面'); } } // 检查是否需要UID校验 if (settings.verifyUid) { log.info('开始:验证切换后的账号UID'); const currentUid = await verifyCurrentUid(); if (isUidMatch(currentUid, settings.targetUid)) { log.info(`切换后的账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,切换成功`); currentSwitchSuccess = true; if (!currentNotificationMessage) { currentNotificationMessage = `成功切换到uid为${settings.targetUid}的账号`; } } else { log.warn(`切换后的账号UID ${currentUid} 与目标UID ${settings.targetUid} 不匹配,需要重新尝试`); currentSwitchSuccess = false; currentNotificationMessage = `切换失败,当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 不匹配`; } } // 如果切换成功,跳出循环 if (currentSwitchSuccess) { switchSuccess = true; notificationMessage = currentNotificationMessage; break; } // 如果是最后一次尝试,保存失败消息 if (attempts === maxAttempts) { notificationMessage = currentNotificationMessage; } } // 发送通知 if (notificationMessage) { if (switchSuccess) { notification.Send(notificationMessage); } else { notification.error(notificationMessage); } } })(); /** * 判断当前所属状态 * 使用广度优先遍历(BFS)从上一个状态开始查找,提高查找效率 * * @param {string|null} previousState - 上一个状态,null表示首次运行或未知状态 * @returns {string|null} 当前状态标识,如果无法确定则返回null */ async function determineCurrentState(previousState = null) { if (!stateMachineConfig) { log.error('状态机配置未加载'); return null; } // 构建BFS遍历序列 const stateSequence = buildBFSSequence(previousState); // 最多尝试3次 const maxAttempts = 30; let currentMousePos = null; // 保存当前鼠标位置,初始为空表示未知 for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { // 遍历所有状态进行匹配 for (const stateName of stateSequence) { const state = stateMachineConfig[stateName]; if (!state || !state.detection) continue; // 评估该状态的检测条件,传入当前鼠标位置 const result = await evaluateDetectionConditions(state.detection, currentMousePos); if (result.isMatch) { return stateName; } // 更新鼠标位置 currentMousePos = result.mousePos; } } catch (e) { log.error(`识别状态时出错:${e.message}`); } if (attempt < maxAttempts) { log.warn(`第 ${attempt} 次尝试未识别到当前状态,1秒后重试...`); await sleep(1000); } } log.error(`经过 ${maxAttempts} 次尝试仍无法识别当前状态`); return null; } /** * 构建BFS遍历序列 * 从previousState开始广度优先遍历,然后处理剩余未遍历节点 * * @param {string|null} previousState - 起始状态 * @returns {string[]} 状态遍历序列 */ function buildBFSSequence(previousState = null) { if (!stateMachineConfig) { log.error('状态机配置未加载'); return []; } const allStates = Object.keys(stateMachineConfig); if (allStates.length === 0) return []; const visited = new Set(); const sequence = []; const queue = []; // 如果有指定起始状态,从它开始BFS if (previousState && stateMachineConfig[previousState]) { queue.push(previousState); visited.add(previousState); } // BFS遍历 while (queue.length > 0) { const currentState = queue.shift(); sequence.push(currentState); const state = stateMachineConfig[currentState]; if (state && state.transitions) { for (const transition of state.transitions) { const targetState = transition.targetState; if (targetState && !visited.has(targetState) && stateMachineConfig[targetState]) { visited.add(targetState); queue.push(targetState); } } } } // 处理未遍历到的节点(从第一个未遍历节点开始继续BFS) for (const stateName of allStates) { if (!visited.has(stateName)) { // 从这个未遍历节点开始新的BFS const subQueue = [stateName]; visited.add(stateName); while (subQueue.length > 0) { const currentState = subQueue.shift(); sequence.push(currentState); const state = stateMachineConfig[currentState]; if (state && state.transitions) { for (const transition of state.transitions) { const targetState = transition.targetState; if (targetState && !visited.has(targetState) && stateMachineConfig[targetState]) { visited.add(targetState); subQueue.push(targetState); } } } } } } return sequence; } /** * 评估状态的检测条件 * 根据配置中的conditions和logic表达式判断当前是否处于该状态 * * @param {Object} detection - 检测配置对象,包含conditions数组和logic表达式 * @returns {boolean} 是否匹配该状态 */ async function evaluateDetectionConditions(detection, currentMousePos) { if (!detection || !detection.conditions || detection.conditions.length === 0) { return { isMatch: false, mousePos: currentMousePos }; } // 检查鼠标位置是否需要移动 let newMousePos = currentMousePos; // 检查当前鼠标位置是否离所有识别区域均超过50x50 const isMouseSafe = checkMousePosition(currentMousePos, detection.conditions); if (!isMouseSafe) { // 寻找合适的鼠标位置 newMousePos = findSafeMousePosition(detection.conditions); if (newMousePos) { moveMouseTo(newMousePos.x, newMousePos.y); await sleep(50); } else { // 没有找到合适的位置,移动到默认位置 moveMouseTo(10, 10); await sleep(50); newMousePos = { x: 10, y: 10 }; } } // 先检查并释放gameRegion if (gameRegion) { gameRegion.dispose(); } // 捕获游戏画面 gameRegion = captureGameRegion(); try { // 计算每个条件的值 const conditionValues = {}; for (const condition of detection.conditions) { const { id, template, region } = condition; // 创建识别对象 const templatePath = template; const ro = RecognitionObject.TemplateMatch( file.ReadImageMatSync(templatePath), region.x, region.y, region.width, region.height ); // 执行识别 const result = gameRegion.find(ro); conditionValues[id] = result.isExist(); } // 使用logic表达式计算最终结果 // 将logic表达式中的条件ID替换为实际值 let logicExpression = detection.logic; for (const [key, value] of Object.entries(conditionValues)) { // 使用正则替换完整的条件名,避免部分匹配 const regex = new RegExp(`\\b${key}\\b`, 'g'); logicExpression = logicExpression.replace(regex, value); } // 计算逻辑表达式 try { const isMatch = eval(logicExpression); return { isMatch, mousePos: newMousePos }; } catch (e) { log.error(`逻辑表达式计算失败: ${detection.logic} -> ${logicExpression}, 错误: ${e.message}`); return { isMatch: false, mousePos: newMousePos }; } } catch (e) { log.error(`评估检测条件时出错:${e.message}`); return { isMatch: false, mousePos: newMousePos }; } finally { if (gameRegion) { gameRegion.dispose(); } } } /** * 检查鼠标位置是否离所有识别区域均超过50x50 * @param {Object|null} mousePos - 当前鼠标位置 {x, y} * @param {Array} conditions - 检测条件数组 * @returns {boolean} 是否安全 */ function checkMousePosition(mousePos, conditions) { if (!mousePos) { return false; // 未知位置,认为不安全 } for (const condition of conditions) { const { region } = condition; const { x, y, width, height } = region; // 检查鼠标是否在识别区域附近50像素内 if (mousePos.x >= x - 50 && mousePos.x <= x + width + 50 && mousePos.y >= y - 50 && mousePos.y <= y + height + 50) { return false; } } return true; } /** * 在10x10到1910x1070间隔10x10的点阵中寻找安全的鼠标位置 * @param {Array} conditions - 检测条件数组 * @returns {Object|null} 安全的鼠标位置 {x, y},如果没有找到返回null */ function findSafeMousePosition(conditions) { for (let x = 10; x <= 1910; x += 10) { for (let y = 10; y <= 1070; y += 10) { let isSafe = true; for (const condition of conditions) { const { region } = condition; const { x: rx, y: ry, width, height } = region; // 检查该点是否在识别区域附近50像素内 if (x >= rx - 50 && x <= rx + width + 50 && y >= ry - 50 && y <= ry + height + 50) { isSafe = false; break; } } if (isSafe) { return { x, y }; } } } return null; // 没有找到安全位置 } /** * 前往指定状态 * 判断当前状态后,执行相应操作前往目标状态,每步执行后重新判断当前状态 * * @param {string} targetState - 目标状态名称 * @param {string|null} previousState - 上一个状态(可选),用于判断状态起点 * @returns {boolean} 是否成功到达目标状态 */ async function goToState(targetState, previousState = null) { if (!stateMachineConfig) { log.error('状态机配置未加载'); return false; } if (!stateMachineConfig[targetState]) { log.error(`目标状态 ${targetState} 不存在于配置中`); return false; } const maxSteps = 20; // 最大步骤数,防止无限循环 let currentState = previousState; let steps = 0; // 防护机制:记录每个状态的尝试次数 const stateAttemptCount = new Map(); const maxStateAttempts = 5; // 同一状态最多尝试5次 while (steps < maxSteps) { // 判断当前状态 const detectedState = await determineCurrentState(currentState); if (!detectedState) { log.error('无法识别当前状态,停止状态切换'); return false; } // 检查是否已到达目标状态 if (detectedState === targetState) { log.info(`已到达目标状态: ${targetState}`); return true; } // 防护机制:检查当前状态的尝试次数 const attemptCount = stateAttemptCount.get(detectedState) || 0; if (attemptCount >= maxStateAttempts) { log.error(`状态 ${detectedState} 已连续尝试 ${attemptCount} 次,超过最大限制,停止状态切换`); log.error('可能陷入状态循环,请检查状态配置或界面状态'); return false; } stateAttemptCount.set(detectedState, attemptCount + 1); // 查找从当前状态到目标状态的路径 const path = findPath(detectedState, targetState); if (!path || path.length === 0) { log.error(`无法找到从 ${detectedState} 到 ${targetState} 的路径`); return false; } // 执行路径中的第一步 const nextState = path[0]; const transition = stateMachineConfig[detectedState].transitions.find( t => t.targetState === nextState ); if (!transition) { log.error(`从 ${detectedState} 到 ${nextState} 的转移未定义`); return false; } // 只在第一次尝试或重试时显示转移信息 if (attemptCount === 0) { log.info(`${detectedState} -> ${nextState} (目标: ${targetState})`); } else { log.warn(`重试: ${detectedState} -> ${nextState} (第 ${attemptCount + 1} 次)`); } try { // 使用 new Function 创建异步函数并执行 const actionFunc = new Function('return (async () => { ' + transition.action + ' })()'); await actionFunc(); } catch (e) { log.error(`执行转移操作失败: ${e.message}`); return false; } // 等待一小段时间让界面响应 await sleep(500); currentState = detectedState; steps++; } log.error(`超过最大步骤数 ${maxSteps},停止状态切换`); return false; } /** * 查找从起始状态到目标状态的路径 * 使用BFS算法查找最短路径 * * @param {string} startState - 起始状态 * @param {string} targetState - 目标状态 * @returns {string[]|null} 路径数组(不包含起始状态,包含目标状态),如果无法到达则返回null */ function findPath(startState, targetState) { if (!stateMachineConfig[startState] || !stateMachineConfig[targetState]) { return null; } if (startState === targetState) { return []; } // BFS查找最短路径 const queue = [[startState]]; const visited = new Set([startState]); while (queue.length > 0) { const path = queue.shift(); const currentState = path[path.length - 1]; const state = stateMachineConfig[currentState]; if (!state || !state.transitions) continue; for (const transition of state.transitions) { const nextState = transition.targetState; if (nextState === targetState) { // 找到路径 return [...path.slice(1), nextState]; } if (!visited.has(nextState) && stateMachineConfig[nextState]) { visited.add(nextState); queue.push([...path, nextState]); } } } return null; // 无法到达目标状态 } /** * 通用找图/找RO并可选点击(支持单图片文件路径、单RO、图片文件路径数组、RO数组) * @param {string|string[]|RecognitionObject|RecognitionObject[]} target * @param {boolean} [doClick=true] 是否点击 * @param {number} [timeout=3000] 识别时间上限(ms) * @param {number} [interval=50] 识别间隔(ms) * @param {number} [retType=0] 0-返回布尔;1-返回 Region 结果 * @param {number} [preClickDelay=50] 点击前等待 * @param {number} [postClickDelay=50] 点击后等待 * @returns {boolean|Region} 根据 retType 返回是否成功或最终 Region */ async function findAndClick(target, doClick = true, timeout = 3000, interval = 50, retType = 0, preClickDelay = 50, postClickDelay = 50) { // 建立识别目标的对象,将 mat 和 ro 分别挂载到对象上 let targetObjs = []; try { // 1. 统一处理目标,保存 mat 和 ro 的对应关系 if (Array.isArray(target)) { targetObjs = new Array(target.length); for (let i = 0; i < target.length; i++) { const t = target[i]; targetObjs[i] = {}; if (typeof t === 'string') { targetObjs[i].mat = file.ReadImageMatSync(t); targetObjs[i].ro = RecognitionObject.TemplateMatch(targetObjs[i].mat); } else { targetObjs[i].ro = t; } } } else { targetObjs = new Array(1); targetObjs[0] = {}; if (typeof target === 'string') { targetObjs[0].mat = file.ReadImageMatSync(target); targetObjs[0].ro = RecognitionObject.TemplateMatch(targetObjs[0].mat); } else { targetObjs[0].ro = target; } } const start = Date.now(); let found = null; while (Date.now() - start <= timeout) { const gameRegion = captureGameRegion(); try { // 依次尝试每一个 ro for (let i = 0; i < targetObjs.length; i++) { const res = gameRegion.find(targetObjs[i].ro); if (!res.isEmpty()) { // 找到 found = res; if (doClick) { await sleep(preClickDelay); res.click(); await sleep(postClickDelay); } break; // 成功即跳出 for } } if (found) break; // 成功即跳出 while } finally { gameRegion.dispose(); } await sleep(interval); // 没找到时等待 } // 3. 按需返回 return retType === 0 ? !!found : (found || null); } catch (error) { log.error(`执行通用识图时出现错误:${error.message}`); return retType === 0 ? false : null; } finally { // 遍历对象释放 mat for (let i = 0; i < targetObjs.length; i++) { if (targetObjs[i] && targetObjs[i].mat) { try { targetObjs[i].mat.dispose(); } catch (e) { log.error(`释放 Mat 对象时出错:${e.message}`); } } } } } // 将函数挂载到 globalThis,供 new Function 创建的作用域访问 //globalThis.findAndClick = findAndClick; /** * 数字模板匹配 * * @param {string} numberPngFilePath - 存放 0.png ~ 9.png 的文件夹路径(不含文件名) * @param {number} x - 待识别区域的左上角 x 坐标,默认 0 * @param {number} y - 待识别区域的左上角 y 坐标,默认 0 * @param {number} w - 待识别区域的宽度,默认 1920 * @param {number} h - 待识别区域的高度,默认 1080 * @param {number} maxThreshold - 模板匹配起始阈值,默认 0.95(最高可信度) * @param {number} minThreshold - 模板匹配最低阈值,默认 0.8(最低可信度) * @param {number} splitCount - 在 maxThreshold 与 minThreshold 之间做几次等间隔阈值递减,默认 5 * @param {number} maxOverlap - 非极大抑制时允许的最大重叠像素,默认 2;只要 x 或 y 方向重叠大于该值即视为重复框 * * @returns {number} 识别出的整数;若没有任何有效数字框则返回 -1 * * @example * const mora = await numberTemplateMatch('摩拉数字', 860, 70, 200, 40); * if (mora >= 0) console.log(`当前摩拉:${mora}`); */ async function numberTemplateMatch( numberPngFilePath, x = 0, y = 0, w = 1920, h = 1080, maxThreshold = 0.95, minThreshold = 0.8, splitCount = 5, maxOverlap = 2 ) { let targetObjs = new Array(10); // 0-9 共10个数字模板 for (let i = 0; i <= 9; i++) { targetObjs[i] = {}; targetObjs[i].mat = file.ReadImageMatSync(`${numberPngFilePath}/${i}.png`); targetObjs[i].ro = RecognitionObject.TemplateMatch(targetObjs[i].mat, x, y, w, h); } function setThreshold(objs, newThreshold) { for (let i = 0; i < objs.length; i++) { if (objs[i] && objs[i].ro) { objs[i].ro.Threshold = newThreshold; objs[i].ro.InitTemplate(); } } } let gameRegion; const allCandidates = []; try { gameRegion = captureGameRegion(); /* 1. splitCount 次等间隔阈值递减 */ for (let k = 0; k < splitCount; k++) { const curThr = maxThreshold - (maxThreshold - minThreshold) * k / Math.max(splitCount - 1, 1); setThreshold(targetObjs, curThr); /* 2. 9-0 每个模板跑一遍,所有框都收 */ for (let digit = 9; digit >= 0; digit--) { try { const res = gameRegion.findMulti(targetObjs[digit].ro); if (res.count === 0) continue; for (let i = 0; i < res.count; i++) { const box = res[i]; allCandidates.push({ digit: digit, x: box.x, y: box.y, w: box.width, h: box.height, thr: curThr }); } } catch (e) { log.error(`识别数字 ${digit} 时出错:${e.message}`); } } } } catch (error) { log.error(`识别数字过程中出现错误:${error.message}`); } finally { if (gameRegion) gameRegion.dispose(); // 释放数字模板的 mat 对象 for (let i = 0; i < targetObjs.length; i++) { if (targetObjs[i] && targetObjs[i].mat) { try { targetObjs[i].mat.dispose(); } catch (e) { log.error(`释放数字模板 Mat 对象时出错:${e.message}`); } } } } /* 3. 无结果提前返回 -1 */ if (allCandidates.length === 0) { return -1; } /* 4. 非极大抑制(必须 x、y 两个方向重叠都 > maxOverlap 才视为重复) */ const adopted = []; for (const c of allCandidates) { let overlap = false; for (const a of adopted) { const xOverlap = Math.max(0, Math.min(c.x + c.w, a.x + a.w) - Math.max(c.x, a.x)); const yOverlap = Math.max(0, Math.min(c.y + c.h, a.y + a.h) - Math.max(c.y, a.y)); if (xOverlap > maxOverlap && yOverlap > maxOverlap) { overlap = true; break; } } if (!overlap) { adopted.push(c); //log.info(`在 [${c.x},${c.y},${c.w},${c.h}] 找到数字 ${c.digit},匹配阈值=${c.thr}`); } } /* 5. 按 x 排序,拼整数;仍无有效框时返回 -1 */ if (adopted.length === 0) return -1; adopted.sort((a, b) => a.x - b.x); return adopted.reduce((num, item) => num * 10 + item.digit, 0); } /** * 计算两个字符串的相似度 * 使用Levenshtein距离算法 * * @param {string} str1 - 第一个字符串 * @param {string} str2 - 第二个字符串 * @returns {number} 相似度,范围0-1 */ function calculateSimilarity(str1, str2) { const len1 = str1.length; const len2 = str2.length; // 创建距离矩阵 const matrix = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0)); // 初始化第一行和第一列 for (let i = 0; i <= len1; i++) { matrix[i][0] = i; } for (let j = 0; j <= len2; j++) { matrix[0][j] = j; } // 计算距离 for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[i][j] = Math.min( matrix[i - 1][j] + 1, // 删除 matrix[i][j - 1] + 1, // 插入 matrix[i - 1][j - 1] + cost // 替换 ); } } // 计算相似度 const maxLen = Math.max(len1, len2); const distance = matrix[len1][len2]; const similarity = 1 - (distance / maxLen); return similarity; } /** * 检查UID是否匹配 * 考虑到识别误差,当相似度大于等于8/9时认为匹配成功 * * @param {number} currentUid - 当前识别的UID * @param {string} targetUid - 目标UID * @returns {boolean} 是否匹配成功 */ function isUidMatch(currentUid, targetUid) { if (currentUid < 0) { return false; } const currentUidStr = currentUid.toString(); const targetUidStr = targetUid.toString(); // 计算相似度 const similarity = calculateSimilarity(currentUidStr, targetUidStr); // 输出相似度信息 log.info(`UID相似度:${(similarity * 100).toFixed(2)}% (${currentUidStr} vs ${targetUidStr})`); // 相似度大于等于8/9时认为匹配成功 return similarity >= 8 / 9; } /** * 校验当前账号UID * * @returns {number} 当前账号UID,若识别失败返回-1 */ async function verifyCurrentUid() { try { // 尝试返回主界面 await genshin.returnMainUi(); // 尝试使用状态机进入主界面 await goToState('mainUI'); // 无论尝试是否成功,按一次G键 keyPress('VK_G'); await sleep(1000); // 识别UID,识别区域是1727 1050 160 30 const uid = await numberTemplateMatch('assets/uid图片', 1727, 1050, 160, 30); if (uid >= 0) { log.info(`成功识别当前账号UID:${uid}`); } else { log.warn('未能识别当前账号UID'); } await genshin.returnMainUi(); return uid; } catch (e) { log.error(`校验UID时出错:${e.message}`); return -1; } } /** * 处理截图模式 * 前往主界面识别UID,然后前往"进入游戏或登录其他账号"界面,截图保存对应UID图片 */ async function handleScreenshotMode() { try { log.info('进入截图模式'); // 1. 确定要使用的UID let uidStr = settings.targetUid; if (!uidStr) { log.info('未设置目标UID,尝试识别当前账号UID'); const currentUid = await verifyCurrentUid(); if (currentUid < 0) { log.error('未能识别当前账号UID,截图模式失败'); notification.error('截图模式失败:未能识别当前账号UID'); return; } uidStr = currentUid.toString(); log.info(`识别到当前账号UID:${uidStr}`); } else { log.info(`使用设置的目标UID:${uidStr}`); } // 2. 前往"进入游戏或登录其他账号"界面 log.info('开始:前往进入游戏或登录其他账号界面'); // 直接尝试进入enterGame状态 const enterGameResult = await goToState('enterGame'); if (!enterGameResult) { log.error('未能到达进入游戏或登录其他账号界面,截图模式失败'); notification.error('截图模式失败:未能到达进入游戏或登录其他账号界面'); return; } log.info('成功到达进入游戏或登录其他账号界面'); // 3. 截图保存对应UID图片 log.info('开始:截图保存账号图片'); // 截图区域:780 481 150 27 const CAP_X = 780; const CAP_Y = 481; const CAP_W = 150; const CAP_H = 27; // 保存路径 const TARGET_DIR = 'accounts'; const fullPath = TARGET_DIR + '/' + uidStr + '.png'; // 捕获游戏画面 gameRegion = captureGameRegion(); try { const mat = gameRegion.DeriveCrop(CAP_X, CAP_Y, CAP_W, CAP_H).SrcMat; // 保存图片 file.WriteImageSync(fullPath, mat); mat.dispose(); } finally { gameRegion.dispose(); gameRegion = null; } log.info(`成功保存账号图片:${fullPath}`); notification.Send(`截图模式成功:已保存账号图片 ${uidStr}.png`); // 4. 返回主界面 log.info('开始:返回主界面'); await goToState('mainUI'); log.info('已返回主界面'); } catch (e) { log.error(`截图模式出错:${e.message}`); notification.error(`截图模式失败:${e.message}`); } }