diff --git a/repo/js/CD-Aware-AutoGather/README.md b/repo/js/CD-Aware-AutoGather/README.md index 4e7693b39..c2e8bdcfc 100644 --- a/repo/js/CD-Aware-AutoGather/README.md +++ b/repo/js/CD-Aware-AutoGather/README.md @@ -1,5 +1,3 @@ -**由于使用了尚处于测试版BetterGI中的API,使用稳定版BetterGI的用户请等待`0.54.1`或更高的版本发布后再订阅此脚本** - (在脚本仓库页面阅读此文档,会比在BGI的已订阅脚本界面获得更好的渲染效果) # 功能特点 @@ -11,6 +9,8 @@ - 可设置一个或多个不运行的时间段 - 采集过程自动切换合适的队伍 +**若脚本有问题,可[点击此处进行反馈](https://github.com/babalae/bettergi-scripts-list/issues/new?template=bug_report.yml&script-name=CD-Aware-AutoGather:2.1.0&additional-info=保留此行以便通知作者:%20@Patrick-Ze%0A%0A---%0A)** + # 使用前准备 **双击运行脚本所在目录下的`SymLink.bat`文件,以创建符号链接。** @@ -171,6 +171,8 @@ - 感谢[this-Fish](https://github.com/this-Fish)的改进,基于坐标判断是否更新记录、将材料是否刷新的检查提前都是沿袭的TA的思路 +- 参考了[吉吉喵](https://github.com/JJMdzh)的背包扫描,增加了使用补充OCR的方式识别物品数量的机制 + 最后,要特别感谢绫华,是她陪伴了我的提瓦特之旅。在弃坑之后,唯有这份牵挂,支撑着我重新回到这里。 没有她就没有今天的这个脚本。 diff --git a/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冬凌草.png b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冬凌草.png new file mode 100644 index 000000000..64e602d1d Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冬凌草.png differ diff --git a/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冷鲜肉.png b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冷鲜肉.png new file mode 100644 index 000000000..e15028f31 Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冷鲜肉.png differ diff --git a/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/奇异的「牙齿」.png b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/奇异的「牙齿」.png new file mode 100644 index 000000000..6b0e68d55 Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/奇异的「牙齿」.png differ diff --git a/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/松珀香.png b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/松珀香.png new file mode 100644 index 000000000..68c0fb53e Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/松珀香.png differ diff --git a/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/红果果菇.png b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/红果果菇.png new file mode 100644 index 000000000..4f77054c9 Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/assets/images/CustomScan/红果果菇.png differ diff --git a/repo/js/CD-Aware-AutoGather/lib/inventory.js b/repo/js/CD-Aware-AutoGather/lib/inventory.js index fa4f4eb1e..070989e2e 100644 --- a/repo/js/CD-Aware-AutoGather/lib/inventory.js +++ b/repo/js/CD-Aware-AutoGather/lib/inventory.js @@ -7,7 +7,7 @@ */ let scriptContext = { - version: "1.0", + version: "1.1", }; // 原本是csv格式,但是为了方便js重用,还是内置在代码中 @@ -81,6 +81,8 @@ const csvText = `物品,刷新机制,背包分类 幽光星星,46小时,材料 月莲,46小时,材料 月落银,46小时,材料 +冬凌草,46小时,材料 +松珀香,46小时,材料 云岩裂叶,46小时,材料 灼灼彩菊,46小时,材料 子探测单元,46小时,材料 @@ -130,6 +132,9 @@ const csvText = `物品,刷新机制,背包分类 烛伞蘑菇,每天0点,食物 ` +const renameMap = { "晶蝶": "晶核", "「冷鲜肉」": "冷鲜肉", "白铁矿": "白铁块", "铁矿": "铁块" }; + +const supportFile = "native_supported.json"; const materialMetadata = {}; function parseCsvTextToDict() { @@ -178,13 +183,110 @@ function parseCsvTextToDict() { return resultDict; } +/** + * 获取需要手动扫描的材料及其对应路径 + * @returns {Object} 格式为 { "材料名": "文件完整路径" } 的对象 + */ +function getManualOcrMaterials() { + const scanDir = "assets/images/CustomScan"; + const items = file.ReadPathSync(scanDir); + + // 1. 读取本地记录文件,获取已支持列表 + let native_supported = {}; + try { + const content = readTextSync(supportFile); + if (content) { + native_supported = JSON.parse(content); + } + } catch (error) { + log.debug(`读取记录文件失败: ${error.toString()}`); + } + + // 2. 核心逻辑:建立材料名与路径的映射关系,并过滤已支持材料 + const manualDict = {}; + for (const itemPath of items) { + // 提取材料名(不含扩展名) + const itemName = splitExt(basename(itemPath))[0]; + // 差集计算:只有当 BetterGI 不支持该材料时,才加入待扫描字典 + if (!native_supported.hasOwnProperty(itemName)) { + manualDict[itemName] = itemPath; + } + } + + return manualDict; +} + +const manualOcrMaterials = getManualOcrMaterials(); + +async function manualOcr(normalizedItemList, shouldStop, sharedResults) { + // 1. 预加载:将所有待识别材料的模板初始化并存储,避免在循环中重复读取文件 + const templates = {}; + let remainingItems = []; + + for (const itemName of normalizedItemList) { + const mat = file.readImageMatSync(manualOcrMaterials[itemName]); + const ro = RecognitionObject.TemplateMatch(mat); + ro.threshold = 0.85; + templates[itemName] = ro; + remainingItems.push(itemName); // 加入待扫描清单 + } + + const _parseInt = (value, defaultValue = 0) => { + const parsed = parseInt(value.trim(), 10); + return Number.isNaN(parsed) ? defaultValue : parsed; + }; + if (remainingItems.length > 0) { + log.info(`将通过补充OCR识别{0}等{1}类物品的数量`, remainingItems[0], remainingItems.length); + } + + // 2. 扫描循环:直到所有材料都找到,或者当前画面不再有新目标 + while (remainingItems.length > 0 && !shouldStop()) { + // 使用 withCapture 确保每一帧 region 都能正确 dispose + const foundInThisFrame = await withCapture(async (region) => { + const foundList = []; + for (const itemName of remainingItems) { + const result = region.find(templates[itemName]); + if (result.isExist()) { + // 计算 OCR 区域坐标, 抄了 吉吉喵 大佬的方法 + const tolerance = 1; + const rect = { + x: result.x - tolerance, + y: result.y + 97 - tolerance, + width: 66 + 2 * tolerance, + height: 22 + 2 * tolerance, + }; + // drawRegion(rect); // 调试用 + let ocrResult = region.find(RecognitionObject.ocr(rect.x, rect.y, rect.width, rect.height)); + if (ocrResult && ocrResult.text) { + const count = _parseInt(ocrResult.text, -2); + sharedResults[itemName] = count; + log.info("{0}: {1}", itemName, count); + foundList.push(itemName); // 记录本帧识别到的条目 + } + } + } + return foundList; // 将本帧找到的列表返回给外部 + }); + + // 3. 更新剩余待扫描列表 + if (foundInThisFrame.length > 0) { + remainingItems = remainingItems.filter((item) => !foundInThisFrame.includes(item)); + } + if (remainingItems.length > 0 && !shouldStop()) { + await sleep(100); // 如果仍有剩余材料,短暂停顿 + } else { + break; // 全部找到,退出循环 + } + } + + return sharedResults; +} + /** * 获取背包中物品的数量。 * 如果没有找到,则为-1;如果找到了但数字识别失败,则为-2 - * - * 暂不支持 冷鲜肉, 红果果菇, 奇异的「牙齿」 */ -async function getItemCount(itemList=null, retry=true) { +async function getItemCount(itemList=null) { if (Object.keys(materialMetadata).length === 0) { Object.assign(materialMetadata, parseCsvTextToDict()); } @@ -194,17 +296,105 @@ async function getItemCount(itemList=null, retry=true) { } else if (itemList == null || itemList.length === 0) { itemList = Object.keys(materialMetadata); } - const renameMap = {"晶蝶": "晶核", "「冷鲜肉」":"冷鲜肉"}; + const normalizedItemList = itemList.map((itemName) => { + return renameMap[itemName] || itemName; + }); + const result = await mainScan(normalizedItemList); + return result; +} + +/** + * 合并扫描结果并实现特征库自动更新(自愈) + * @param {Object} nativeResults - BetterGI 返回的结果 { "鸣草": 10, ... } + * @param {Object} manualResults - 手动 OCR 返回的结果 { "新材料": 5, ... } + * @param {string[]} targetManualList - 本次扫描中,原本判定为需要“手动处理”的材料名清单 + * @returns {Object} 合并后的最终清单 + */ +function mergeAndAutoRepair(nativeResults, manualResults, manualCandidateList) { + // 1. 以原生结果为基础,合并手动结果 + const finalData = { ...nativeResults }; + const newlySupported = []; + + for (const name in manualResults) { + // 如果 nativeResults 里没有这个材料,采用手动结果 + if (!finalData[name] || finalData[name] < 0) { + finalData[name] = manualResults[name]; + } + } + + // 2. 自愈检查:遍历原本以为需要手动的列表 + for (const name of manualCandidateList) { + // 如果 BetterGI 的返回结果里包含了这个材料说明特征库更新了 + if (nativeResults.hasOwnProperty(name) && nativeResults[name] > 0) { + newlySupported.push(name); + } + } + + // 3. 更新本地记录文件 + if (newlySupported.length > 0) { + updateNativeSupportedJson(newlySupported); + } + + return finalData; +} + +/** + * 更新本地受支持列表的持久化存储 + */ +function updateNativeSupportedJson(newItems) { + let registry = {}; + try { + registry = JSON.parse(readTextSync(supportFile) || "{}"); + } catch(e) {} + + const today = new Date().toISOString().split('T')[0]; + newItems.forEach(item => { + registry[item] = today; + log.info(`发现{0}已被内置接口支持,未来将跳过它的补充OCR`, item); + }); + + file.WriteTextSync(supportFile, JSON.stringify(registry, null, 2)); +} + +async function mainScan(normalizedItemList) { + const sharedOcrResults = {}; // 创建共享对象 + let isApiFinished = false; + + const manualList = normalizedItemList.filter(name => manualOcrMaterials.hasOwnProperty(name)); + + // 并发启动 + const apiPromise = getItemCountWithApi(normalizedItemList, sharedOcrResults) + .finally(() => isApiFinished = true); + const manualPromise = manualOcr(manualList, () => isApiFinished, sharedOcrResults); + + // 等待全部完成 + const [apiResults, _] = await Promise.all([apiPromise, manualPromise]); + + // 最后合并结果(此时 sharedOcrResults 已经包含了所有 OCR 成功识别的内容) + const results = mergeAndAutoRepair(apiResults, sharedOcrResults, manualList); + const finalResults = {}; + const reverseRenameMap = Object.entries(renameMap).reduce((acc, [key, value]) => { + acc[value] = key; + return acc; + }, {}); + for (const itemName of normalizedItemList) { + // 如果某个元素没有找到,则不会存在对应的键值,赋值为-1以保持与单个物品查找时一致的行为 + const originName = reverseRenameMap[itemName] || itemName; + finalResults[originName] = results[itemName] ?? -1; + } + return finalResults; +} + +async function getItemCountWithApi(normalizedItemList, sharedOcrResults) { const groupByType = {}; - for (const itemName of itemList) { + for (const itemName of normalizedItemList) { const metadata = materialMetadata[itemName]; const itemType = metadata?.type ?? "Materials"; if (!metadata?.type) { log.warn("未查找到{0}所属的背包分类,默认它是{1}", itemName, itemType); } - const normalizedName = renameMap[itemName] || itemName; groupByType[itemType] = groupByType[itemType] || []; - groupByType[itemType].push(normalizedName); + groupByType[itemType].push(itemName); } let results = {}; @@ -216,26 +406,38 @@ async function getItemCount(itemList=null, retry=true) { })); Object.assign(results, countResult); } - if (retry && itemList.some(item => !(item in results))) { + + // 逻辑:既不在 API 的结果里,也不在 OCR 已经实时识别到的结果里 + const missingItems = normalizedItemList.filter(name => + !(name in results) && !(name in sharedOcrResults) + ); + if (missingItems.length > 0) { // 即使在白天,大多数情况也能识别成功,因此不作为常态机制,仅在失败时使用 - log.info("部分物品识别失败,调整时间和视角后重试"); + log.info(`${missingItems.length}个物品识别失败,调整时间和视角后重试`); await genshin.returnMainUi(); await genshin.setTime(0, 0); await sleep(300); moveMouseBy(0, 9280); await sleep(300); - const retryResults = await getItemCount(itemList, false); + + // 只针对缺失的物品进行重试 + for (const type in groupByType) { + const namesToRetry = groupByType[type].filter(name => missingItems.includes(name)); + if (namesToRetry.length > 0) { + const retryCountResult = await dispatcher.runTask(new SoloTask("CountInventoryItem", { + "gridScreenName": type, + "itemNames": namesToRetry, + })); + // 将重试结果合并到原始 results 中 + Object.assign(results, retryCountResult); + } + } + + // 恢复视角 await genshin.returnMainUi(); keyPress("MBUTTON"); - return retryResults; } - const finalResults = {}; - for (const itemName of itemList) { - const normalizedName = renameMap[itemName] || itemName; - // 如果某个元素没有找到,则不会存在对应的键值,赋值为-1以保持与单个物品查找时一致的行为 - finalResults[itemName] = results[normalizedName] ?? -1; - } - return finalResults; + return results; } function getItemCD(itemName) { diff --git a/repo/js/CD-Aware-AutoGather/lib/ocr.js b/repo/js/CD-Aware-AutoGather/lib/ocr.js index 90ec5efc3..a582e0304 100644 --- a/repo/js/CD-Aware-AutoGather/lib/ocr.js +++ b/repo/js/CD-Aware-AutoGather/lib/ocr.js @@ -1,200 +1,214 @@ -const defaultReplacementMap = { +// 模块级变量,存储默认映射表 +let replacementMap = { 监: "盐", 卵: "卯", }; -/** - * 绘制方框以标记OCR区域 - * - * @param {number[4] | {x: number, y: number, width: number, height: number}} ocrRegion - OCR 区域,数组或对象形式,表示 [x, y, width, height] - * @param {object|null} [captureRegion=null] - 截图区域对象。为 null 时自动调用 captureGameRegion() - * @throws {TypeError} 当 ocrRegion 既不是数组也不是对象时抛出。 - */ -function drawOcrRegion(ocrRegion, captureRegion = null) { - let x, y, width, height; - if (Array.isArray(ocrRegion)) { - [x, y, width, height] = ocrRegion; - } else if (typeof ocrRegion === "object" && ocrRegion !== null) { - ({ x, y, width, height } = ocrRegion); - } else { - throw new TypeError("'ocrRegion' must be an array [x, y, width, height] or an object with x, y, width, height properties"); - } - - let auto_created = false; - if (captureRegion === null) { - captureRegion = captureGameRegion(); - auto_created = true; - } - let region = captureRegion.DeriveCrop(x, y, width, height); - let name = [x, y, width, height].toString(); - region.DrawSelf(name); - region.dispose(); - if (auto_created) { - captureRegion.dispose(); +/** 允许用户在运行时动态修改库的默认映射表 */ +function setGlobalReplacementMap(newMap) { + if (typeof newMap === "object" && newMap !== null) { + replacementMap = newMap; } } -async function getTextInRegion(ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) { - let x, y, width, height; - if (Array.isArray(ocrRegion)) { - [x, y, width, height] = ocrRegion; - } else if (typeof ocrRegion === "object" && ocrRegion !== null) { - ({ x, y, width, height } = ocrRegion); - } else { - throw new Error("Invalid parameter 'ocrRegion'"); +/** + * 内部私有工具:校正文本 + */ +function _correctText(text, customMap) { + if (!text) return ""; + const map = customMap || replacementMap; // 如果没传自定义的,就用默认的 + let correctedText = text; + for (const [wrong, right] of Object.entries(map)) { + correctedText = correctedText.replace(new RegExp(wrong, "g"), right); } + return correctedText.trim(); +} +/** + * 解析坐标 + * @param {Array|Object} area - 坐标 [x, y, w, h] 或 {x, y, width, height} + */ +function parseRect(area) { + let x, y, width, height; + if (Array.isArray(area)) { + [x, y, width, height] = area; + } else if (typeof area === "object" && area !== null) { + ({ x, y, width, height } = area); + } else { + throw new TypeError("Invalid area format"); + } + return { x, y, width, height }; +} + +/** + * 屏幕截图包装器 + * @param {Function} action - 接收 region 对象的异步函数 + */ +async function withCapture(action) { + const region = captureGameRegion(); + try { + return await action(region); + } finally { + region.dispose?.(); + } +} + +/** + * 局部裁剪包装器 + * @param {object} region - 父级截图区域 + * @param {object} rect - {x, y, width, height} + * @param {Function} action - 接收 crop 对象的异步函数 + */ +async function withCrop(region, rect, action) { + const crop = region.DeriveCrop(rect.x, rect.y, rect.width, rect.height); + try { + return await action(crop); + } finally { + crop.dispose?.(); + } +} + +/** + * 安全地绘制区域标识(自动处理坐标解析与资源释放) + * @param {Array|Object} area - 坐标 [x, y, w, h] 或 {x, y, width, height} + * @param {object} [existingFrame=null] - (可选) 已有的截图对象。如果不传,则自动创建并释放新截图。 + * @param {string} label - 绘制在框上的标签名 + */ +async function drawRegion(area, existingFrame = null, label = null) { + const rect = parseRect(area); + + // 内部绘制逻辑:只负责裁切和画图 + const doDraw = async (f) => { + await withCrop(f, rect, async (crop) => { + const mark = label ? label : `rect_${rect.x}_${rect.y}_${rect.width}_${rect.height}`; + crop.DrawSelf(mark); + }); + }; + + if (existingFrame) { + // 如果外部传了 frame,我们只负责释放 crop,不释放外部的 frame + await doDraw(existingFrame); + } else { + // 如果没传,我们截一张新图,并在画完释放自己截的这张图 + await withCapture(async (tempFrame) => { + await doDraw(tempFrame); + }); + } +} + +/** + * 快速判断区域内是否存在文本(单次检查) + */ +async function isTextExistedInRegion(searchText, ocrRegion) { + const rect = parseRect(ocrRegion); + + return await withCapture(async (captureRegion) => { + const result = captureRegion.find(RecognitionObject.ocr(rect.x, rect.y, rect.width, rect.height)); + if (!result || !result.text) return false; + + const text = result.text; + return (searchText instanceof RegExp) ? !!text.match(searchText) : text.includes(searchText); + }); +} + + +/** + * 获取区域内的文本(带重试机制) + */ +async function getTextInRegion(ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = null) { + const rect = parseRect(ocrRegion); const debugThreshold = timeout / retryInterval / 3; - let startTime = Date.now(); - let retryCount = 0; // 重试计数 - while (Date.now() - startTime < timeout) { - let captureRegion = captureGameRegion(); - try { - // 尝试 OCR 识别 - let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域 - // 遍历识别结果,检查是否找到目标文本 - for (let res of resList) { - // 后处理:根据替换映射表检查和替换错误识别的字符 - let correctedText = res.text; - for (let [wrongChar, correctChar] of Object.entries(replacementMap)) { - correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar); - } + const startTime = Date.now(); + let retryCount = 0; - captureRegion.dispose(); - return correctedText.trim(); + while (Date.now() - startTime < timeout) { + // 使用 withCapture 自动管理截图资源的生命周期 + const result = await withCapture(async (captureRegion) => { + try { + const resList = captureRegion.findMulti(RecognitionObject.ocr(rect.x, rect.y, rect.width, rect.height)); + // resList 通常不是真正的 JS Array,使用 .count 且使用下标访问 + const count = resList.count || resList.Count || 0; + for (let i = 0; i < count; i++) { + const res = resList[i]; + if (!res || !res.text) continue; + const corrected = _correctText(res.text, replacementMap); + // 如果识别到了有效文本(不为空),则返回 + if (corrected) { + return corrected; + } + } + } catch (error) { + log.warn(`OCR 识别失败,正在进行第 ${retryCount} 次重试...`); } - } catch (error) { - log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`); - } - retryCount++; // 增加重试计数 - if (retryCount > debugThreshold) { - let region = captureRegion.DeriveCrop(x, y, width, height); - region.DrawSelf("debug"); - region.dispose(); - } - captureRegion.dispose(); + + // 达到调试阈值时画框 + if (++retryCount > debugThreshold) { + await drawRegion(rect, captureRegion, "debug"); + } + return null; + }); + + if (result !== null) return result; await sleep(retryInterval); } return null; } -async function waitForTextAppear(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) { - let x, y, width, height; - - if (Array.isArray(ocrRegion)) { - [x, y, width, height] = ocrRegion; - } else if (typeof ocrRegion === "object" && ocrRegion !== null) { - ({ x, y, width, height } = ocrRegion); - } else { - throw new Error("Invalid parameter 'ocrRegion'"); - } - - const debugThreshold = timeout / retryInterval / 3; - let startTime = Date.now(); - let retryCount = 0; // 重试计数 +/** + * 等待特定文本出现 + */ +async function waitForTextAppear(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = null) { + const startTime = Date.now(); + + // 循环复用 getTextInRegion 的逻辑思想 while (Date.now() - startTime < timeout) { - let captureRegion = captureGameRegion(); - try { - // 尝试 OCR 识别 - let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域 - // 遍历识别结果,检查是否找到目标文本 - for (let res of resList) { - // 后处理:根据替换映射表检查和替换错误识别的字符 - let correctedText = res.text; - for (let [wrongChar, correctChar] of Object.entries(replacementMap)) { - correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar); - } - - if (correctedText.includes(targetText)) { - captureRegion.dispose(); - return { success: true, wait_time: Date.now() - startTime }; - } - } - } catch (error) { - log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`); + const currentText = await getTextInRegion(ocrRegion, retryInterval, retryInterval, replacementMap); + if (currentText && currentText.includes(targetText)) { + return { success: true, wait_time: Date.now() - startTime }; } - retryCount++; // 增加重试计数 - if (retryCount > debugThreshold) { - let region = captureRegion.DeriveCrop(x, y, width, height); - region.DrawSelf("debug"); - region.dispose(); - } - captureRegion.dispose(); - await sleep(retryInterval); + // 此处不需要额外 sleep,因为 getTextInRegion 内部已经耗费了时间 } return { success: false }; } -async function recognizeTextAndClick(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) { - let x, y, width, height; - - if (Array.isArray(ocrRegion)) { - [x, y, width, height] = ocrRegion; - } else if (typeof ocrRegion === "object" && ocrRegion !== null) { - ({ x, y, width, height } = ocrRegion); - } else { - throw new Error("Invalid parameter 'ocrRegion'"); - } - +/** + * 识别文本并点击中心点 + */ +async function recognizeTextAndClick(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = null) { + const rect = parseRect(ocrRegion); const debugThreshold = timeout / retryInterval / 3; - let startTime = Date.now(); - let retryCount = 0; // 重试计数 - while (Date.now() - startTime < timeout) { - let captureRegion = captureGameRegion(); - try { - // 尝试 OCR 识别 - let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域 - // 遍历识别结果,检查是否找到目标文本 - for (let res of resList) { - // 后处理:根据替换映射表检查和替换错误识别的字符 - let correctedText = res.text; - for (let [wrongChar, correctChar] of Object.entries(replacementMap)) { - correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar); - } + const startTime = Date.now(); + let retryCount = 0; - if (correctedText.includes(targetText)) { - // 如果找到目标文本,计算并点击文字的中心坐标 - let centerX = Math.round(res.x + res.width / 2); - let centerY = Math.round(res.y + res.height / 2); - await click(centerX, centerY); - await sleep(50); - captureRegion.dispose(); - return { success: true, x: centerX, y: centerY }; + while (Date.now() - startTime < timeout) { + const clicked = await withCapture(async (captureRegion) => { + try { + const resList = captureRegion.findMulti(RecognitionObject.ocr(rect.x, rect.y, rect.width, rect.height)); + // resList 通常不是真正的 JS Array,使用 .count 且使用下标访问 + const count = resList.count || resList.Count || 0; + for (let i = 0; i < count; i++) { + const res = resList[i]; + const correctedText = _correctText(res.text, replacementMap); + if (correctedText.includes(targetText)) { + const centerX = Math.round(res.x + res.width / 2); + const centerY = Math.round(res.y + res.height / 2); + await click(centerX, centerY); + await sleep(50); + return { success: true, x: centerX, y: centerY }; + } } + } catch (e) { + log.warn(`识别点击失败重试中...`); } - } catch (error) { - log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`); - } - retryCount++; // 增加重试计数 - if (retryCount > debugThreshold) { - let region = captureRegion.DeriveCrop(x, y, width, height); - region.DrawSelf("debug"); - region.dispose(); - } - captureRegion.dispose(); + + if (++retryCount > debugThreshold) { + await drawRegion(rect, captureRegion); + } + return null; + }); + + if (clicked) return clicked; await sleep(retryInterval); } return { success: false }; } - -async function isTextExistedInRegion(searchText, ocrRegion) { - let x, y, width, height; - if (Array.isArray(ocrRegion)) { - [x, y, width, height] = ocrRegion; - } else if (typeof ocrRegion === "object" && ocrRegion !== null) { - ({ x, y, width, height } = ocrRegion); - } else { - throw new Error("Invalid parameter 'ocrRegion'"); - } - let captureRegion = captureGameRegion(); - const result = captureRegion.find(RecognitionObject.ocr(x, y, width, height)); - captureRegion.dispose(); - if (result.text) { - if (typeof searchText === "string") { - return result.text.includes(searchText); - } else if (searchText instanceof RegExp) { - return result.text.match(searchText); - } - } - return false; -} diff --git a/repo/js/CD-Aware-AutoGather/main.js b/repo/js/CD-Aware-AutoGather/main.js index c97d64b30..0eed75ba1 100644 --- a/repo/js/CD-Aware-AutoGather/main.js +++ b/repo/js/CD-Aware-AutoGather/main.js @@ -229,7 +229,13 @@ async function runGatherMode() { const sortedTasksToRun = sortTasksByGap(groupedTasksToRun); // file.writeTextSync("sortedTasksToRun.json", JSON.stringify(sortedTasksToRun, null, 2)); - log.info("共{0}种材料需要采集,将从缺失数量最多的材料开始", Object.keys(sortedTasksToRun).length); + const taskCount = Object.keys(sortedTasksToRun).length; + if (taskCount === 0) { + log.info("所有材料的数量均已达标"); + return; + } + + log.info("共{0}种材料需要采集,将从缺失数量最多的材料开始", taskCount); for (const [name, { target, current, tasks }] of Object.entries(sortedTasksToRun)) { const coolType = tasks[0].coolType; const targetTxt = target === null ? "∞" : target; diff --git a/repo/js/CD-Aware-AutoGather/manifest.json b/repo/js/CD-Aware-AutoGather/manifest.json index 49539d1a0..27b0b529b 100644 --- a/repo/js/CD-Aware-AutoGather/manifest.json +++ b/repo/js/CD-Aware-AutoGather/manifest.json @@ -1,8 +1,9 @@ { "manifest_version": 1, "name": "带CD管理和目标设定的自动采集", - "version": "2.0.2", - "bgi_version": "0.54.1-alpha.1", + "_version_note": "更新版本号时请一并更新README的问题反馈链接中的版本号", + "version": "2.1.0", + "bgi_version": "0.55.0", "description": "自动同步你通过BetterGI订阅的地图追踪任务,执行采集任务,并管理材料采集目标和刷新时间。\n支持联机模式采集和多账号记录。\n首次使用前请先简单阅读说明", "authors": [ {