mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-26 05:09:52 +08:00
js: CD-Aware-AutoGather: 使用OCR识别CountInventoryItem尚不支持的材料 (#2738)
其他改进: - 改进背包扫描重试机制 - 包装`captureGameRegion`为带上下文管理的函数
This commit is contained in:
@@ -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的方式识别物品数量的机制
|
||||
|
||||
最后,要特别感谢绫华,是她陪伴了我的提瓦特之旅。在弃坑之后,唯有这份牵挂,支撑着我重新回到这里。
|
||||
|
||||
没有她就没有今天的这个脚本。
|
||||
|
||||
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冬凌草.png
Normal file
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冬凌草.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冷鲜肉.png
Normal file
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/冷鲜肉.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/奇异的「牙齿」.png
Normal file
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/奇异的「牙齿」.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/松珀香.png
Normal file
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/松珀香.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/红果果菇.png
Normal file
BIN
repo/js/CD-Aware-AutoGather/assets/images/CustomScan/红果果菇.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user