js: CD-Aware-AutoGather: 使用OCR识别CountInventoryItem尚不支持的材料 (#2738)

其他改进:
- 改进背包扫描重试机制
- 包装`captureGameRegion`为带上下文管理的函数
This commit is contained in:
Patrick-Ze
2026-01-20 16:17:18 +08:00
committed by GitHub
parent e562e75bf7
commit 92051f5370
10 changed files with 418 additions and 193 deletions

View File

@@ -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的方式识别物品数量的机制
最后,要特别感谢绫华,是她陪伴了我的提瓦特之旅。在弃坑之后,唯有这份牵挂,支撑着我重新回到这里。
没有她就没有今天的这个脚本。

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -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) {

View File

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

View File

@@ -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;

View File

@@ -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": [
{