mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-05-09 00:44:17 +08:00
背包采集统计优化修复 (#2853)
* 诸多优化修复 优化背包扫图逻辑;修复自动拾取匹配bug,改为双向匹配手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错 * Add files via upload * fix typo. --------- Co-authored-by: 起个名字好难的喵 <25520958+MisakaAldrich@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 背包材料统计 v2.54
|
||||
# 背包材料统计 v2.60
|
||||
作者:吉吉喵
|
||||
|
||||
<!-- 新增:全局图片样式,控制连续图片同行显示 -->
|
||||
@@ -128,7 +128,7 @@
|
||||
1. **禁止联机请求**:联机请求会遮挡背包菜单,导致材料识别失败。建议在本脚本前添加「AutoPermission」权限设置JS(仓库可查),默认禁止联机;
|
||||
2. **文件夹层级限制**:`pathing` 文件夹仅支持3层子文件夹,超过需手动削减(否则路径无法读取);
|
||||
3. **食物识别强制要求**:背包食物界面**第一行必须包含8种食物**(苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪),缺少则这些食物无法识别;
|
||||
4. **关键文件备份**:建议不定期备份 `pathing` 文件夹(路径文件)和 `pathing_record` 文件夹(路径运行记录),便于丢失后或记录被污染后恢复如初;
|
||||
4. **关键文件备份**:建议不定期备份 `pathing` 文件夹(路径文件)和 `pathing_record` 文件夹(路径运行记录),便于丢失后或被污染后,记录能恢复如初;
|
||||
5. **OCR配置**:默认最新,调整识别名单时,用的是V5Auto;
|
||||
6. **手动终止运行**:如果要终止JS运行,推荐在当前路径采集到当前材料前,或者采集完进入背包扫描时终止(会在扫描结束后终止),以保护当前记录;如果是【取消扫描】模式,不会储存当前记录的材料数目,就随意。
|
||||
|
||||
@@ -184,7 +184,7 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
|
||||
- `overwrite_record`:所有历史记录(按材料分类储存);
|
||||
- `history_record`:勾选“材料分类”后的专属记录;
|
||||
- `latest_record.txt`:最近几种材料的记录(有上限,仅存最新数据);
|
||||
- `pathing_record`:单个路径的完整记录(含运行时间、收获量,需重点备份),材料收集汇总.txt(始末差值记录),标准名-0.txt(0收获记录);
|
||||
- `pathing_record`:单个路径的完整记录(含运行时间、收获量,需重点备份),材料收集汇总.txt(始末差值记录),标准名-0.txt(0收获记录,三次及以上同名路径记录,就会触发排除);
|
||||
操作参考截图:
|
||||
<div class="img-row-container">
|
||||
<img src="assets/Pic/Pic13.png" alt="本地记录存放位置参考截图" class="img-row-item">
|
||||
@@ -224,4 +224,10 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
|
||||
| v2.50 | 新增独立名单拾取、弹窗模块;支持怪物名识别 |
|
||||
| v2.51 | 自定义设置新增“拖动距离/拖动点”;新增月卡弹窗识别;路径材料超量自动上黑名单;修复怪物0收获记录 |
|
||||
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |
|
||||
| v2.54 | 自定义设置新增“终止时刻”,修复bug,新增“添加不规范命名的路径文件夹”说明,新增一个“锄地”的怪物路径CD |
|
||||
| v2.54 | 自定义设置新增“终止时刻”,修复bug,新增“添加不规范命名的路径文件夹”说明,新增一个“锄地”的怪物路径CD |
|
||||
| v2.55 | 修复超量名单不生效 |
|
||||
| v2.56 | 更新诺德卡莱图包 |
|
||||
| v2.57 | 补全圣遗物无CD管理bug |
|
||||
| v2.58 | 优化背包扫图逻辑 |
|
||||
| v2.59 | 修复自动拾取匹配bug,改为双向匹配 |
|
||||
| v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错 |
|
||||
@@ -1,10 +1,8 @@
|
||||
/*
|
||||
|
||||
// 自动拾取逻辑
|
||||
|
||||
// 自动拾取逻辑(基于最新版performOcr改造)
|
||||
*/
|
||||
|
||||
// 解析文件内容,提取分类信息
|
||||
// 解析文件内容,提取分类信息(无修改,与OCR无关)
|
||||
function parseCategoryContent(content) {
|
||||
if (!content) {
|
||||
log.warn(`文件内容为空`);
|
||||
@@ -25,9 +23,9 @@ function parseCategoryContent(content) {
|
||||
|
||||
return categories;
|
||||
}
|
||||
// 从 targetText 文件夹中读取分类信息
|
||||
|
||||
// 从 targetText 文件夹中读取分类信息(无修改,与OCR无关)
|
||||
function readtargetTextCategories(targetTextDir) {
|
||||
// log.info(`开始读取材料分类信息:${targetTextDir}`);
|
||||
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1);
|
||||
const materialCategories = {};
|
||||
|
||||
@@ -35,33 +33,34 @@ function readtargetTextCategories(targetTextDir) {
|
||||
const pickTextNames = (settings.PickCategories || "")
|
||||
.split(/[,,、 \s]+/).map(n => n.trim()).filter(n => n);
|
||||
|
||||
// 【新增:兜底日志】确认pickTextNames是否为空,方便排查
|
||||
// 兜底日志:确认pickTextNames是否为空,方便排查
|
||||
log.info(`筛选名单状态:${pickTextNames.length === 0 ? '未指定(空),将加载所有文件' : '指定了:' + pickTextNames.join(',')}`);
|
||||
|
||||
for (const filePath of targetTextFilePaths) {
|
||||
if (state.cancelRequested) break;
|
||||
const content = file.readTextSync(filePath);
|
||||
const content = safeReadTextSync(filePath);
|
||||
if (!content) {
|
||||
log.error(`加载文件失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名
|
||||
// 【核心筛选:空名单直接跳过判断,加载所有】
|
||||
const sourceCategory = basename(filePath).replace('.txt', '');
|
||||
|
||||
// 核心筛选:空名单直接加载所有,非空名单仅加载指定
|
||||
if (pickTextNames.length === 0) {
|
||||
// 空名单时,直接保留当前文件,不跳过
|
||||
// 空名单时保留当前文件
|
||||
} else if (!pickTextNames.includes(sourceCategory)) {
|
||||
// 非空名单且不在列表里,才跳过
|
||||
// 非空名单且不在列表中则跳过
|
||||
continue;
|
||||
}
|
||||
|
||||
materialCategories[sourceCategory] = parseCategoryContent(content);
|
||||
}
|
||||
log.info(`完成读取,加载的分类:${Object.keys(materialCategories).join(',')}`);
|
||||
return materialCategories;
|
||||
}
|
||||
|
||||
// const OCRdelay = Math.min(100, Math.max(0, Math.floor(Number(settings.OcrDelay) || 2))); // F识别基准时长
|
||||
|
||||
// 尝试找到 F 图标并返回其坐标
|
||||
// 尝试找到目标图标(F图标/Scroll.png通用)并返回其坐标(无修改,与OCR无关)
|
||||
async function findFIcon(recognitionObject, timeout = 10, ra = null) {
|
||||
let startTime = Date.now();
|
||||
while (Date.now() - startTime < timeout && !state.cancelRequested) {
|
||||
@@ -77,7 +76,7 @@ async function findFIcon(recognitionObject, timeout = 10, ra = null) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
await sleep(5); // 每次检测间隔 5 毫秒
|
||||
await sleep(5); // 每次检测间隔5毫秒
|
||||
}
|
||||
if (state.cancelRequested) {
|
||||
log.info("图标识别任务已取消");
|
||||
@@ -85,14 +84,14 @@ async function findFIcon(recognitionObject, timeout = 10, ra = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 定义Scroll.png识别对象(按需求使用TemplateMatch,包含指定范围)
|
||||
// 定义Scroll.png识别对象(无修改,与OCR无关)
|
||||
const ScrollRo = RecognitionObject.TemplateMatch(
|
||||
file.ReadImageMatSync("assets/Scroll.png"),
|
||||
file.ReadImageMatSync("assets/Scroll.png"),
|
||||
1055, 521, 15, 35 // 识别范围:x=1055, y=521, width=15, height=35
|
||||
);
|
||||
|
||||
/**
|
||||
* 对齐并交互目标(直接用findFIcon识别Scroll.png)
|
||||
* 对齐并交互目标(核心改造:适配最新版performOcr)
|
||||
* @param {string[]} targetTexts - 待匹配的目标文本列表
|
||||
* @param {Object} fDialogueRo - F图标的识别对象
|
||||
* @param {Object} textxRange - 文本识别的X轴范围 { min: number, max: number }
|
||||
@@ -106,6 +105,7 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
|
||||
try {
|
||||
while (!state.completed && !state.cancelRequested) {
|
||||
recognitionCount.clear(); // 每次循环开始时清空计数
|
||||
const currentTime = Date.now();
|
||||
// 每10秒输出检测日志(保留原逻辑)
|
||||
if (currentTime - lastLogTime >= 10000) {
|
||||
@@ -121,15 +121,16 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
}
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
// 尝试找到 F 图标
|
||||
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
|
||||
if (!fRes) {
|
||||
const scrollRes = await findFIcon(ScrollRo, 10, cachedFrame); // 复用findFIcon函数
|
||||
if (scrollRes) {
|
||||
await keyMouseScript.runFile(`assets/滚轮下翻.json`); // 调用翻滚脚本
|
||||
// 2. 识别F图标/Scroll.png(保留原逻辑)
|
||||
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
|
||||
if (!fRes) {
|
||||
const scrollRes = await findFIcon(ScrollRo, 10, cachedFrame);
|
||||
if (scrollRes) {
|
||||
log.debug("未识别到F图标,但识别到Scroll.png,执行翻滚操作");
|
||||
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue; // 继续下一轮检测
|
||||
}
|
||||
|
||||
// 3. 核心改造:调用最新版performOcr
|
||||
// 适配点1:参数顺序调整为「targetTexts, xRange, yRange, ra, timeout, interval」
|
||||
@@ -145,17 +146,19 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
);
|
||||
ocrScreenshots.push(ocrScreenshot); // 收集截图,避免内存泄漏
|
||||
|
||||
// 4. 文本匹配与交互(保留原逻辑,无修改)
|
||||
// 4. 文本匹配与交互(双向匹配,增强容错性)
|
||||
let foundTarget = false;
|
||||
for (const targetText of targetTexts) {
|
||||
const targetResult = ocrResults.find(res => res.text.includes(targetText));
|
||||
const targetResult = ocrResults.find(res =>
|
||||
res.text.includes(targetText) || targetText.includes(res.text)
|
||||
);
|
||||
if (!targetResult) continue;
|
||||
|
||||
// 计数防误触(原逻辑)
|
||||
// 计数防误触
|
||||
const materialId = `${targetText}-${targetResult.y}`;
|
||||
recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1);
|
||||
|
||||
// Y轴对齐判断(原逻辑)
|
||||
// Y轴对齐判断
|
||||
const centerYTargetText = targetResult.y + targetResult.height / 2;
|
||||
if (Math.abs(centerYTargetText - (fRes.y + fRes.height / 2)) <= texttolerance) {
|
||||
if (recognitionCount.get(materialId) >= 1) {
|
||||
@@ -163,7 +166,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
log.info(`交互或拾取: ${targetText}`);
|
||||
recognitionCount.delete(materialId);
|
||||
}
|
||||
|
||||
foundTarget = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
eval(file.readTextSync("lib/region.js"));
|
||||
eval(safeReadTextSync("lib/region.js"));
|
||||
|
||||
const holdX = Math.min(1920, Math.max(0, Math.floor(Number(settings.HoldX) || 1050)));
|
||||
const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 750)));
|
||||
@@ -10,6 +10,7 @@ const pageScrollCount = 22; // 最多滑页次数
|
||||
|
||||
// 材料分类映射表
|
||||
const materialTypeMap = {
|
||||
"祝圣精华": "2",
|
||||
"锻造素材": "5",
|
||||
"怪物掉落素材": "3",
|
||||
"一般素材": "5",
|
||||
@@ -22,12 +23,11 @@ const materialTypeMap = {
|
||||
"角色天赋素材": "3",
|
||||
"武器突破素材": "3",
|
||||
"采集食物": "4",
|
||||
"料理": "4",
|
||||
"料理": "4"
|
||||
};
|
||||
|
||||
// 材料前位定义
|
||||
const materialPriority = {
|
||||
"养成道具": 1,
|
||||
"祝圣精华": 2,
|
||||
"锻造素材": 1,
|
||||
"怪物掉落素材": 1,
|
||||
@@ -41,7 +41,7 @@ const materialPriority = {
|
||||
"宝石": 4,
|
||||
"鱼饵鱼类": 5,
|
||||
"角色天赋素材": 5,
|
||||
"武器突破素材": 6,
|
||||
"武器突破素材": 6
|
||||
};
|
||||
|
||||
// 2. 数字替换映射表(处理OCR识别误差)
|
||||
@@ -132,6 +132,89 @@ async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) {
|
||||
});
|
||||
}
|
||||
|
||||
// 并行模板匹配函数
|
||||
async function parallelTemplateMatch(ra, materials, x, y, width, height, threshold = 0.8, enableMouseMove = true) {
|
||||
const matchPromises = materials.map(({ name, mat }) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, x, y, width, height);
|
||||
recognitionObject.threshold = threshold;
|
||||
recognitionObject.Use3Channels = true;
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
if (enableMouseMove) {
|
||||
moveMouseTo(result.x, result.y);
|
||||
}
|
||||
resolve({ name, result });
|
||||
} else {
|
||||
resolve({ name, result: null });
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`并行模板匹配错误: ${name} - ${error.message}`);
|
||||
resolve({ name, result: null });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return await Promise.all(matchPromises);
|
||||
}
|
||||
|
||||
// 智能OCR识别函数
|
||||
async function smartRecognizeText(ocrRegion, ra, quickMode = true) {
|
||||
if (quickMode) {
|
||||
// 快速模式:超时50ms,只尝试1次
|
||||
return await recognizeText(ocrRegion, 50, 5, 1, 1, ra);
|
||||
} else {
|
||||
// 正常模式:超时100ms,尝试3次
|
||||
return await recognizeText(ocrRegion, 100, 10, 3, 2, ra);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量OCR处理函数
|
||||
async function batchRecognizeText(ocrRegions, ra, timeout = 200, retryInterval = 10, maxAttempts = 5, maxFailures = 2, quickMode = true) {
|
||||
const ocrPromises = ocrRegions.map(({ region, name }) => {
|
||||
return new Promise((resolve) => {
|
||||
smartRecognizeText(region, ra, quickMode)
|
||||
.then(result => resolve({ name, result }))
|
||||
.catch(error => {
|
||||
log.error(`批量OCR错误: ${name} - ${error.message}`);
|
||||
resolve({ name, result: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return await Promise.all(ocrPromises);
|
||||
}
|
||||
|
||||
// 合并OCR区域函数(用于密集区域的批量处理)
|
||||
function mergeOcrRegions(regions) {
|
||||
if (regions.length === 0) return [];
|
||||
|
||||
// 按y坐标排序
|
||||
regions.sort((a, b) => a.region.y - b.region.y);
|
||||
|
||||
const merged = [];
|
||||
let currentMerge = { ...regions[0] };
|
||||
|
||||
for (let i = 1; i < regions.length; i++) {
|
||||
const region = regions[i];
|
||||
// 检查是否可以合并(垂直距离小于阈值)
|
||||
if (region.region.y - (currentMerge.region.y + currentMerge.region.height) < 20) {
|
||||
// 合并区域
|
||||
currentMerge.region.width = Math.max(currentMerge.region.width, region.region.width);
|
||||
currentMerge.region.height = region.region.y + region.region.height - currentMerge.region.y;
|
||||
currentMerge.name += `,${region.name}`;
|
||||
} else {
|
||||
merged.push(currentMerge);
|
||||
currentMerge = { ...region };
|
||||
}
|
||||
}
|
||||
merged.push(currentMerge);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// 通用鼠标拖动函数(提取重复逻辑)
|
||||
/**
|
||||
* 通用鼠标拖动工具函数
|
||||
@@ -210,21 +293,23 @@ function filterMaterialsByPriority(materialsCategory) {
|
||||
throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`);
|
||||
}
|
||||
|
||||
// 获取所有优先级更高的材料分类(前位材料)
|
||||
const frontPriorityMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType);
|
||||
|
||||
// 获取所有优先级更低的材料分类(后位材料)
|
||||
const backPriorityMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType);
|
||||
// 合并当前和后位材料分类
|
||||
const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料
|
||||
|
||||
// 合并当前和后位材料分类(只包含同位和后位,不包含前位)
|
||||
// 只有同位或后位材料才会触发全列扫描
|
||||
const finalFilteredMaterials = [...backPriorityMaterials, materialsCategory];
|
||||
return finalFilteredMaterials
|
||||
}
|
||||
|
||||
// 扫描材料
|
||||
// 扫描材料
|
||||
async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
// 获取当前+后位材料名单
|
||||
// 材料图片缓存
|
||||
const materialImages = {}; // 用于缓存加载的图片
|
||||
const priorityMaterialImages = {}; // 用于缓存优先级材料图片
|
||||
|
||||
// 获取当前+后位材料名单(仅包含同位和后位,不包含前位)
|
||||
const priorityMaterialNames = [];
|
||||
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
|
||||
for (const category of finalFilteredMaterials) {
|
||||
@@ -233,6 +318,14 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
for (const filePath of materialIconFilePaths) {
|
||||
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
|
||||
priorityMaterialNames.push({ category, name });
|
||||
|
||||
// 预加载优先级材料图片
|
||||
if (!priorityMaterialImages[name]) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (!mat.empty()) {
|
||||
priorityMaterialImages[name] = mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +338,6 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
// 创建材料种类集合
|
||||
const materialCategories = [];
|
||||
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
|
||||
const materialImages = {}; // 用于缓存加载的图片
|
||||
|
||||
// 检查 materialCategoryMap 中当前分类的数组是否为空
|
||||
const categoryMaterials = materialCategoryMap[materialsCategory] || [];
|
||||
@@ -285,7 +377,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
const columnHeight = 680;
|
||||
const maxColumns = 8;
|
||||
// 跟踪已截图的区域(避免重复)
|
||||
const capturedRegions = new Set();
|
||||
const capturedRegions = new Set();
|
||||
|
||||
// 扫描状态
|
||||
let hasFoundFirstMaterial = false;
|
||||
@@ -320,80 +412,96 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
let tempPhrases = [...scanPhrases];
|
||||
tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性
|
||||
let phrasesStartTime = Date.now();
|
||||
let previousScreenshot = null; // 用于存储上一次翻页前的截图
|
||||
// 扫描背包中的材料
|
||||
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
|
||||
|
||||
const ra = captureGameRegion();
|
||||
if (!foundPriorityMaterial) {
|
||||
for (const { category, name } of priorityMaterialNames) {
|
||||
if (recognizedMaterials.has(name)) {
|
||||
continue; // 如果已经识别过,跳过
|
||||
}
|
||||
// 重置foundPriorityMaterial标志,每次翻页都重新检查
|
||||
foundPriorityMaterial = false;
|
||||
|
||||
const filePath = `assets/images/${category}/${name}.png`;
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载材料图库失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
}
|
||||
// 检查第八列是否有目标材料
|
||||
// priorityMaterialNames只包含当前和后位材料
|
||||
const priorityMaterialsToMatch = priorityMaterialNames
|
||||
.filter(({ name }) => !recognizedMaterials.has(name))
|
||||
.map(({ name }) => {
|
||||
const mat = priorityMaterialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1142, startY, columnWidth, columnHeight);
|
||||
recognitionObject.threshold = 0.8; // 设置识别阈值
|
||||
recognitionObject.Use3Channels = true;
|
||||
if (priorityMaterialsToMatch.length > 0) {
|
||||
const matchResults = await parallelTemplateMatch(ra, priorityMaterialsToMatch, 1142, startY, columnWidth, columnHeight, 0.8);
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
foundPriorityMaterial = true; // 标记找到前位材料
|
||||
// drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除
|
||||
log.info(`发现当前或后位材料: ${name},开始全列扫描`);
|
||||
break; // 发现前位材料后,退出当前循环
|
||||
for (const { name, result } of matchResults) {
|
||||
if (result) {
|
||||
foundPriorityMaterial = true; // 标记找到目标材料
|
||||
// log.info(`发现目标材料: ${name},开始全列扫描`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有发现目标材料时,才执行全列扫描
|
||||
if (foundPriorityMaterial) {
|
||||
log.info(`开始全列扫描`);
|
||||
const ocrRegions = [];
|
||||
const matchedMaterials = [];
|
||||
|
||||
for (let column = 0; column < maxColumns; column++) {
|
||||
const scanX0 = startX + column * OffsetWidth;
|
||||
const scanX = Math.round(scanX0);
|
||||
|
||||
// 准备当前列需要扫描的材料
|
||||
const materialsToMatch = materialCategories
|
||||
.filter(({ name }) => !recognizedMaterials.has(name))
|
||||
.map(({ name }) => {
|
||||
const mat = materialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (let i = 0; i < materialCategories.length; i++) {
|
||||
const { name } = materialCategories[i];
|
||||
if (recognizedMaterials.has(name)) {
|
||||
continue; // 如果已经识别过,跳过
|
||||
}
|
||||
if (materialsToMatch.length > 0) {
|
||||
// 并行扫描当前列的所有材料
|
||||
const matchResults = await parallelTemplateMatch(ra, materialsToMatch, scanX, startY, columnWidth, columnHeight, 0.85);
|
||||
|
||||
const mat = materialImages[name];
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
|
||||
recognitionObject.threshold = 0.85;
|
||||
recognitionObject.Use3Channels = true;
|
||||
// 收集匹配结果和OCR区域
|
||||
for (const { name, result } of matchResults) {
|
||||
if (result) {
|
||||
recognizedMaterials.add(name);
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
// drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除
|
||||
const ocrRegion = {
|
||||
x: result.x - tolerance,
|
||||
y: result.y + 97 - tolerance,
|
||||
width: 66 + 2 * tolerance,
|
||||
height: 22 + 2 * tolerance
|
||||
};
|
||||
ocrRegions.push({ region: ocrRegion, name });
|
||||
matchedMaterials.push({ name, result });
|
||||
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
recognizedMaterials.add(name);
|
||||
moveMouseTo(result.x, result.y);
|
||||
|
||||
// drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除
|
||||
const ocrRegion = {
|
||||
x: result.x - tolerance,
|
||||
y: result.y + 97 - tolerance,
|
||||
width: 66 + 2 * tolerance,
|
||||
height: 22 + 2 * tolerance
|
||||
};
|
||||
const ocrResult = await recognizeText(ocrRegion, 200, 10, 5, 2, ra);
|
||||
materialInfo.push({ name, count: ocrResult || "?" });
|
||||
|
||||
if (!hasFoundFirstMaterial) {
|
||||
hasFoundFirstMaterial = true;
|
||||
lastFoundTime = Date.now();
|
||||
} else {
|
||||
lastFoundTime = Date.now();
|
||||
if (!hasFoundFirstMaterial) {
|
||||
hasFoundFirstMaterial = true;
|
||||
lastFoundTime = Date.now();
|
||||
} else {
|
||||
lastFoundTime = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
await sleep(imageDelay);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理OCR
|
||||
if (ocrRegions.length > 0) {
|
||||
const ocrResults = await batchRecognizeText(ocrRegions, ra);
|
||||
|
||||
// 处理OCR结果
|
||||
for (const { name, result } of ocrResults) {
|
||||
materialInfo.push({ name, count: result || "?" });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info(`未发现目标材料,跳过`);
|
||||
}
|
||||
|
||||
// 每5秒输出一句俏皮话
|
||||
@@ -421,21 +529,110 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查是否到达最后一页
|
||||
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
|
||||
sliderBottomRo.threshold = 0.8;
|
||||
const sliderBottomResult = ra.find(sliderBottomRo);
|
||||
if (sliderBottomResult.isExist()) {
|
||||
log.info("已到达最后一页!");
|
||||
shouldEndScan = true;
|
||||
break;
|
||||
// 检查是否到达最后一页(使用画面比较替代滑条检测)
|
||||
// 注:画面比较逻辑已集成到翻页操作中
|
||||
|
||||
|
||||
// 捕获当前翻页前的截图(用于与下一次翻页前的截图比较)
|
||||
const currentScreenshot = {
|
||||
region: { x: 400, y: 400, width: 600, height: 100 }, // 长条形状比较区域
|
||||
mat: ra.DeriveCrop(400, 400, 600, 100).SrcMat // 外扩1像素
|
||||
};
|
||||
|
||||
// 检查是否需要启动画面比较兜底逻辑
|
||||
let useScreenComparison = false;
|
||||
if (!shouldEndScan && scroll < pageScrollCount && previousScreenshot) {
|
||||
// 检查是否满足兜底条件:
|
||||
// 1. 已找到至少一个材料,但长时间未发现新材料
|
||||
// 2. 或连续多次未发现任何材料
|
||||
if (recognizedMaterials.size > 0) {
|
||||
const noNewMaterialTime = Date.now() - lastFoundTime;
|
||||
if (noNewMaterialTime > 5000) { // 5秒未发现新材料
|
||||
useScreenComparison = true;
|
||||
log.info(`5秒未发现新材料,启动画面比较兜底逻辑`);
|
||||
}
|
||||
} else if (scroll > 5) { // 连续翻页5次以上仍未发现任何材料
|
||||
useScreenComparison = true;
|
||||
log.info(`连续翻页${scroll}次未发现任何材料,启动画面比较兜底逻辑`);
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动到下一页
|
||||
if (scroll < pageScrollCount) {
|
||||
if (useScreenComparison && previousScreenshot) {
|
||||
// 使用模板匹配比较两次翻页前的截图(兜底机制)
|
||||
const matchRo = RecognitionObject.TemplateMatch(
|
||||
previousScreenshot.mat,
|
||||
previousScreenshot.region.x - 1,
|
||||
previousScreenshot.region.y - 1,
|
||||
previousScreenshot.region.width + 2,
|
||||
previousScreenshot.region.height + 2
|
||||
);
|
||||
matchRo.threshold = 0.95; // 高阈值,确保区域变化足够明显
|
||||
matchRo.Use3Channels = true;
|
||||
|
||||
const matchResult = ra.find(matchRo);
|
||||
if (matchResult.isExist()) {
|
||||
log.info("连续翻页画面无明显变化,执行最后一次全列扫描");
|
||||
|
||||
// 执行最后一次全列扫描
|
||||
log.info("执行最后一次全列扫描");
|
||||
for (let column = 0; column < maxColumns; column++) {
|
||||
const scanX0 = startX + column * OffsetWidth;
|
||||
const scanX = Math.round(scanX0);
|
||||
for (let i = 0; i < materialCategories.length; i++) {
|
||||
const { name } = materialCategories[i];
|
||||
if (recognizedMaterials.has(name)) {
|
||||
continue; // 如果已经识别过,跳过
|
||||
}
|
||||
|
||||
const mat = materialImages[name];
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
|
||||
recognitionObject.threshold = 0.85;
|
||||
recognitionObject.Use3Channels = true;
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
recognizedMaterials.add(name);
|
||||
moveMouseTo(result.x, result.y);
|
||||
|
||||
const ocrRegion = {
|
||||
x: result.x - tolerance,
|
||||
y: result.y + 97 - tolerance,
|
||||
width: 66 + 2 * tolerance,
|
||||
height: 22 + 2 * tolerance
|
||||
};
|
||||
const ocrResult = await recognizeText(ocrRegion, 200, 10, 5, 2, ra);
|
||||
materialInfo.push({ name, count: ocrResult || "?" });
|
||||
|
||||
if (!hasFoundFirstMaterial) {
|
||||
hasFoundFirstMaterial = true;
|
||||
lastFoundTime = Date.now();
|
||||
} else {
|
||||
lastFoundTime = Date.now();
|
||||
}
|
||||
}
|
||||
await sleep(imageDelay);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("最后一次全列扫描完成,结束扫描");
|
||||
shouldEndScan = true;
|
||||
break;
|
||||
} else {
|
||||
log.info("连续翻页画面有变化,继续扫描");
|
||||
}
|
||||
}
|
||||
|
||||
// 执行翻页
|
||||
await scrollPage(-totalPageDistance, 10, 5);
|
||||
await sleep(100);
|
||||
// 减少等待时间,提高翻页速度
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
// 更新上一次的截图
|
||||
previousScreenshot = currentScreenshot;
|
||||
}
|
||||
|
||||
// 处理未匹配的材料
|
||||
@@ -461,7 +658,7 @@ ${Array.from(unmatchedMaterialNames).join(",")}
|
||||
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存
|
||||
const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合
|
||||
if (pathingMode.onlyCategory) {
|
||||
writeFile(categoryFilePath, logContent);
|
||||
writeFile(categoryFilePath, logContent);
|
||||
}
|
||||
writeFile(overwriteFilePath, logContent);
|
||||
writeFile(latestFilePath, logContent); // 覆盖模式?
|
||||
@@ -472,10 +669,40 @@ ${Array.from(unmatchedMaterialNames).join(",")}
|
||||
|
||||
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
|
||||
const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38);
|
||||
const XPRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/XP.png"), 653, 29, 38, 38);
|
||||
const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38);
|
||||
const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/CultivationItems.png"), 749, 30, 38, 38);
|
||||
const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38);
|
||||
|
||||
/**
|
||||
* 获取材料分类对应的CategoryObject
|
||||
* @param {string} materialsCategory - 材料分类名称
|
||||
* @returns {Object|null} 对应的CategoryObject或null
|
||||
*/
|
||||
function getCategoryObject(materialsCategory) {
|
||||
switch (materialsCategory) {
|
||||
case "祝圣精华":
|
||||
return XPRo;
|
||||
case "锻造素材":
|
||||
case "一般素材":
|
||||
case "烹饪食材":
|
||||
case "木材":
|
||||
case "鱼饵鱼类":
|
||||
return MaterialsRo;
|
||||
case "采集食物":
|
||||
case "料理":
|
||||
return FoodRo;
|
||||
case "怪物掉落素材":
|
||||
case "周本素材":
|
||||
case "角色突破素材":
|
||||
case "宝石":
|
||||
case "角色天赋素材":
|
||||
case "武器突破素材":
|
||||
return CultivationItemsRo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function dynamicMaterialGrouping(materialCategoryMap) {
|
||||
// 初始化动态分组对象
|
||||
@@ -528,11 +755,18 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
let materialsCategory = ""; // 当前处理的材料分类名称
|
||||
const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息
|
||||
|
||||
// 添加状态变量,记录上一个分类的信息
|
||||
let prevCategory = null;
|
||||
let prevCategoryObject = null;
|
||||
let prevPriority = null;
|
||||
let prevGroup = null;
|
||||
let skipSliderReset = false; // 是否跳过滑条重置
|
||||
|
||||
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
|
||||
// log.info("材料 动态[分组]结果:");
|
||||
// log.info("材料 动态[分组]结果:");
|
||||
sortedGroups.forEach(group => {
|
||||
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
|
||||
});
|
||||
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
|
||||
});
|
||||
|
||||
let loopCount = 0;
|
||||
const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环
|
||||
@@ -550,7 +784,7 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
case 1: // 打开背包界面
|
||||
// log.info("打开背包界面");
|
||||
keyPress("B"); // 打开背包界面
|
||||
await sleep(1000);
|
||||
await sleep(800); // 减少等待时间
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
@@ -595,33 +829,13 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
break;
|
||||
|
||||
case 3: // 识别材料分类
|
||||
let CategoryObject;
|
||||
switch (materialsCategory) {
|
||||
case "锻造素材":
|
||||
case "一般素材":
|
||||
case "烹饪食材":
|
||||
case "木材":
|
||||
case "鱼饵鱼类":
|
||||
CategoryObject = MaterialsRo;
|
||||
break;
|
||||
case "采集食物":
|
||||
case "料理":
|
||||
CategoryObject = FoodRo;
|
||||
break;
|
||||
case "怪物掉落素材":
|
||||
case "周本素材":
|
||||
case "角色突破素材":
|
||||
case "宝石":
|
||||
case "角色天赋素材":
|
||||
case "武器突破素材":
|
||||
CategoryObject = CultivationItemsRo;
|
||||
break;
|
||||
default:
|
||||
log.error("未知的材料分类");
|
||||
// ===== 补充优化:异常时释放资源并退出 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退到阶段0
|
||||
return;
|
||||
let CategoryObject = getCategoryObject(materialsCategory);
|
||||
if (!CategoryObject) {
|
||||
log.error("未知的材料分类");
|
||||
// ===== 补充优化:异常时释放资源并退出 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退到阶段0
|
||||
return;
|
||||
}
|
||||
|
||||
const CategoryResult = await recognizeImage(CategoryObject, cachedFrame);
|
||||
@@ -639,18 +853,63 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
|
||||
case 4: // 扫描材料
|
||||
log.info("芭芭拉,冲鸭!");
|
||||
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
|
||||
await sleep(200);
|
||||
leftButtonDown(); // 长按左键重置材料滑条
|
||||
await sleep(300);
|
||||
leftButtonUp();
|
||||
await sleep(200);
|
||||
|
||||
// 判断是否需要重置滑条
|
||||
if (!skipSliderReset) {
|
||||
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
|
||||
await sleep(200);
|
||||
leftButtonDown(); // 长按左键重置材料滑条
|
||||
await sleep(300);
|
||||
leftButtonUp();
|
||||
await sleep(200);
|
||||
} else {
|
||||
log.info("同一大类且为后位材料,跳过滑条重置");
|
||||
// 不重置滑条,直接从当前位置开始检查第八列
|
||||
}
|
||||
|
||||
// 扫描材料并获取低于目标数量的材料
|
||||
const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap);
|
||||
allLowCountMaterials.push(lowCountMaterials);
|
||||
|
||||
// 保存当前分类信息,用于下一个分类的判断
|
||||
prevCategory = materialsCategory;
|
||||
prevPriority = materialPriority[materialsCategory];
|
||||
|
||||
// 获取当前分类的CategoryObject
|
||||
const currentCategoryObject = getCategoryObject(materialsCategory);
|
||||
prevCategoryObject = currentCategoryObject;
|
||||
prevGroup = sortedGroups[currentGroupIndex];
|
||||
|
||||
currentCategoryIndex++;
|
||||
|
||||
// 判断下一个分类是否是同一个大类CategoryObject下的后位材料
|
||||
let nextCategory = null;
|
||||
let nextCategoryObject = null;
|
||||
let nextPriority = null;
|
||||
|
||||
// 检查是否还有下一个分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
nextCategory = group.categories[currentCategoryIndex];
|
||||
|
||||
// 获取下一个分类的CategoryObject
|
||||
nextCategoryObject = getCategoryObject(nextCategory);
|
||||
|
||||
// 获取下一个分类的优先级
|
||||
nextPriority = materialPriority[nextCategory];
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否跳过滑条重置:同一大类且为后位材料
|
||||
if (nextCategory &&
|
||||
nextCategoryObject === prevCategoryObject &&
|
||||
nextPriority > prevPriority) {
|
||||
skipSliderReset = true;
|
||||
} else {
|
||||
skipSliderReset = false;
|
||||
}
|
||||
|
||||
stage = 2; // 返回阶段2处理下一个分类
|
||||
break;
|
||||
|
||||
|
||||
@@ -14,15 +14,6 @@ function basename(filePath) {
|
||||
return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// 如果路径存在且返回的是数组,则认为是目录Folder
|
||||
function pathExists(path) {
|
||||
try { return file.readPathSync(path)?.length >= 0; }
|
||||
catch { return false; }
|
||||
}
|
||||
*/
|
||||
|
||||
function pathExists(path) {
|
||||
try {
|
||||
return file.isFolder(path);
|
||||
@@ -31,6 +22,12 @@ function pathExists(path) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// 如果路径存在且返回的是数组,则认为是目录Folder
|
||||
function pathExists(path) {
|
||||
try { return file.readPathSync(path)?.length >= 0; }
|
||||
catch { return false; }
|
||||
}
|
||||
// 判断文件是否存在(非目录且能读取)
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
@@ -45,6 +42,93 @@ function fileExists(filePath) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// 判断文件是否存在(基于readPathSync + 已有工具函数,不读取文件内容)
|
||||
function fileExists(filePath) {
|
||||
// 1. 基础参数校验(复用已有逻辑)
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
log.debug(`[文件检查] 路径无效:${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 路径标准化(复用已有normalizePath,统一分隔符)
|
||||
const normalizedFilePath = normalizePath(filePath);
|
||||
|
||||
// 3. 拆分「文件所在目录」和「文件名」(核心步骤)
|
||||
// 3.1 提取纯文件名(复用已有basename)
|
||||
const fileName = basename(normalizedFilePath);
|
||||
// 3.2 提取文件所在的目录路径(基于标准化路径拆分)
|
||||
// 修复:当没有目录结构时,使用'.'表示当前目录,而不是'/'(避免越界访问)
|
||||
const dirPath = normalizedFilePath.lastIndexOf('/') !== -1
|
||||
? normalizedFilePath.substring(0, normalizedFilePath.lastIndexOf('/'))
|
||||
: '.';
|
||||
|
||||
// 4. 先判断目录是否存在
|
||||
if (!pathExists(dirPath)) {
|
||||
log.debug(`[文件检查] 文件所在目录不存在:${dirPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 用readPathSync读取目录下的所有一级子项
|
||||
const dirItems = file.readPathSync(dirPath);
|
||||
if (!dirItems || dirItems.length === 0) {
|
||||
log.debug(`[文件检查] 目录为空,无目标文件:${dirPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let isFileExist = false;
|
||||
for (let i = 0; i < dirItems.length; i++) {
|
||||
const item = dirItems[i];
|
||||
const normalizedItem = normalizePath(item);
|
||||
if (pathExists(normalizedItem)) {
|
||||
continue;
|
||||
}
|
||||
const itemFileName = basename(normalizedItem);
|
||||
if (normalizedItem === normalizedFilePath || itemFileName === fileName) {
|
||||
isFileExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 日志反馈结果
|
||||
if (isFileExist) {
|
||||
// log.debug(`[文件检查] 文件存在:${filePath}`);
|
||||
} else {
|
||||
// log.debug(`[文件检查] 文件不存在:${filePath}`);
|
||||
}
|
||||
return isFileExist;
|
||||
|
||||
} catch (error) {
|
||||
// 捕获目录读取失败等异常
|
||||
log.debug(`[文件检查] 检查失败:${filePath},错误:${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 安全读取文本文件(封装存在性校验+异常处理)
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {any} defaultValue - 读取失败/文件不存在时的默认返回值(默认空字符串)
|
||||
* @returns {any} 文件内容(成功)| defaultValue(失败)
|
||||
*/
|
||||
function safeReadTextSync(filePath, defaultValue = "") {
|
||||
try {
|
||||
// 第一步:校验文件是否存在
|
||||
if (!fileExists(filePath)) {
|
||||
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}文件不存在,跳过读取: ${filePath}`);
|
||||
return defaultValue;
|
||||
}
|
||||
// 第二步:读取文件(捕获读取异常)
|
||||
const content = file.readTextSync(filePath);
|
||||
// log.debug(`${CONSTANTS.LOG_MODULES.RECORD}成功读取文件: ${filePath}`);
|
||||
return content;
|
||||
} catch (error) {
|
||||
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}读取文件失败: ${filePath} → 原因:${error.message}`);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 带深度限制的非递归文件夹读取
|
||||
function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.png', '.json', '.txt'], includeDirs = false) {
|
||||
if (!pathExists(dir)) {
|
||||
@@ -55,11 +139,11 @@ function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.p
|
||||
try {
|
||||
const filePaths = [];
|
||||
const stack = [[dir, depth]]; // 存储(路径, 当前深度)的栈
|
||||
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [currentDir, currentDepth] = stack.pop();
|
||||
const entries = file.readPathSync(currentDir);
|
||||
|
||||
|
||||
for (const entry of entries) {
|
||||
const isDirectory = pathExists(entry);
|
||||
if (isDirectory) {
|
||||
@@ -85,36 +169,36 @@ function writeFile(filePath, content, isAppend = true, maxRecords = 36500) {
|
||||
// 读取现有内容,处理文件不存在的情况
|
||||
let existingContent = "";
|
||||
try {
|
||||
existingContent = file.readTextSync(filePath);
|
||||
existingContent = safeReadTextSync(filePath);
|
||||
} catch (err) {
|
||||
// 文件不存在时视为空内容
|
||||
existingContent = "";
|
||||
}
|
||||
|
||||
|
||||
// 分割现有记录并过滤空记录
|
||||
const records = existingContent.split("\n\n").filter(Boolean);
|
||||
|
||||
|
||||
// 新内容放在最前面,形成完整记录列表
|
||||
const allRecords = [content, ...records];
|
||||
|
||||
|
||||
// 只保留最新的maxRecords条(超过则删除最老的)
|
||||
const keptRecords = allRecords.slice(0, maxRecords);
|
||||
|
||||
|
||||
// 拼接记录并写入文件
|
||||
const finalContent = keptRecords.join("\n\n");
|
||||
const result = file.WriteTextSync(filePath, finalContent, false);
|
||||
|
||||
const result = file.writeTextSync(filePath, finalContent, false);
|
||||
|
||||
// log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
} else {
|
||||
// 覆盖模式直接写入
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
const result = file.writeTextSync(filePath, content, false);
|
||||
// log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// 出错时尝试创建/写入文件
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
const result = file.writeTextSync(filePath, content, false);
|
||||
log.info(result ? `[新建/恢复] 成功处理: ${filePath}` : `[新建/恢复] 处理失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ async function preloadImageResources(specificNames) {
|
||||
|
||||
function hasIconFolder(dirPath) {
|
||||
try {
|
||||
const entries = readAllFilePaths(dirPath, 0, 0, [], true);
|
||||
const entries = readAllFilePaths(dirPath, 0, 0, [], true);
|
||||
return entries.some(entry => normalizePath(entry).endsWith('/icon'));
|
||||
} catch (e) {
|
||||
log.error(`检查目录【${dirPath}】是否有icon文件夹失败:${e.message}`);
|
||||
@@ -24,7 +24,7 @@ async function preloadImageResources(specificNames) {
|
||||
|
||||
const targetDirs = subDirs.filter(subDir => {
|
||||
const dirName = basename(subDir);
|
||||
const hasIcon = hasIconFolder(subDir);
|
||||
const hasIcon = hasIconFolder(subDir);
|
||||
const matchName = isAll ? true : preSpecificNames.includes(dirName);
|
||||
return hasIcon && matchName;
|
||||
});
|
||||
@@ -60,16 +60,16 @@ async function preloadImageResources(specificNames) {
|
||||
|
||||
if (fileExists(configPath)) {
|
||||
try {
|
||||
const configContent = file.readTextSync(configPath);
|
||||
const configContent = safeReadTextSync(configPath);
|
||||
popupConfig = { ...popupConfig, ...JSON.parse(configContent) };
|
||||
isSpecialModule = popupConfig.isSpecial === true
|
||||
&& typeof popupConfig.detectRegion === 'object'
|
||||
&& popupConfig.detectRegion !== null
|
||||
&& popupConfig.detectRegion.x != null
|
||||
&& popupConfig.detectRegion.y != null
|
||||
&& popupConfig.detectRegion.width != null
|
||||
&& popupConfig.detectRegion.height != null
|
||||
&& popupConfig.detectRegion.width > 0
|
||||
isSpecialModule = popupConfig.isSpecial === true
|
||||
&& typeof popupConfig.detectRegion === 'object'
|
||||
&& popupConfig.detectRegion !== null
|
||||
&& popupConfig.detectRegion.x != null
|
||||
&& popupConfig.detectRegion.y != null
|
||||
&& popupConfig.detectRegion.width != null
|
||||
&& popupConfig.detectRegion.height != null
|
||||
&& popupConfig.detectRegion.width > 0
|
||||
&& popupConfig.detectRegion.height > 0;
|
||||
specialDetectRegion = isSpecialModule ? popupConfig.detectRegion : null;
|
||||
// log.info(`【${dirName}】加载配置成功:${isFirstLevel ? '第一级' : '第二级'} | 模块类型:${isSpecialModule ? '特殊模块' : '普通模块'}`);
|
||||
@@ -83,7 +83,7 @@ async function preloadImageResources(specificNames) {
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => normalizePath(entry).endsWith('/icon'));
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
|
||||
if (iconFilePaths.length === 0) {
|
||||
log.warn(`【${dirName}】特殊模块无有效icon文件,跳过`);
|
||||
continue;
|
||||
@@ -96,7 +96,7 @@ async function preloadImageResources(specificNames) {
|
||||
log.error(`【${dirName}】特殊模块加载图标失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
iconRecognitionObjects.push({
|
||||
iconRecognitionObjects.push({
|
||||
name: basename(filePath),
|
||||
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
|
||||
iconDir,
|
||||
@@ -104,7 +104,6 @@ async function preloadImageResources(specificNames) {
|
||||
});
|
||||
}
|
||||
|
||||
// 关键修改:遍历所有图标,为每个图标生成识别信息
|
||||
const foundRegions = []; // 存储所有图标的识别配置
|
||||
for (const targetIcon of iconRecognitionObjects) { // 遍历每个图标
|
||||
const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y);
|
||||
@@ -132,7 +131,7 @@ async function preloadImageResources(specificNames) {
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => normalizePath(entry).endsWith('/icon'));
|
||||
const pictureDir = entries.find(entry => normalizePath(entry).endsWith('/Picture'));
|
||||
|
||||
|
||||
if (!pictureDir) {
|
||||
log.warn(`【${dirName}】普通模块无Picture文件夹,跳过`);
|
||||
continue;
|
||||
@@ -140,7 +139,7 @@ async function preloadImageResources(specificNames) {
|
||||
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
|
||||
// 仅在资源为空时警告
|
||||
if (iconFilePaths.length === 0) {
|
||||
log.warn(`【${dirName}】普通模块无有效icon文件,跳过`);
|
||||
@@ -158,10 +157,10 @@ async function preloadImageResources(specificNames) {
|
||||
log.error(`【${dirName}】加载图标失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
iconRecognitionObjects.push({
|
||||
iconRecognitionObjects.push({
|
||||
name: basename(filePath),
|
||||
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
|
||||
iconDir
|
||||
iconDir
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,9 +171,9 @@ async function preloadImageResources(specificNames) {
|
||||
log.error(`【${dirName}】加载图库图片失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
pictureRegions.push({
|
||||
pictureRegions.push({
|
||||
name: basename(filePath),
|
||||
region: new ImageRegion(mat, 0, 0)
|
||||
region: new ImageRegion(mat, 0, 0)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,7 +216,7 @@ async function imageClickBackgroundTask() {
|
||||
log.info("imageClick后台任务已启动");
|
||||
|
||||
// 配置参数
|
||||
const taskDelay = Math.min(999, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 15)))*1000;
|
||||
const taskDelay = Math.min(999, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 15))) * 1000;
|
||||
const specificNamesStr = settings.PopupNames || "";
|
||||
const specificNames = specificNamesStr
|
||||
.split(/[,,、 \s]+/)
|
||||
@@ -241,10 +240,10 @@ async function imageClickBackgroundTask() {
|
||||
// 打印资源检测结果
|
||||
log.info("\n==================== 现有弹窗加载结果 ====================");
|
||||
log.info("1. 一级弹窗(共" + firstLevelDirs.length + "个):");
|
||||
firstLevelDirs.forEach((res, idx) => log.info(` ${idx+1}. 【${res.dirName}】`));
|
||||
firstLevelDirs.forEach((res, idx) => log.info(` ${idx + 1}. 【${res.dirName}】`));
|
||||
const secondLevelResources = preloadedResources.filter(res => !res.isFirstLevel);
|
||||
log.info("\n2. 二级弹窗(共" + secondLevelResources.length + "个):");
|
||||
secondLevelResources.forEach((res, idx) => log.info(` ${idx+1}. 【${res.dirName}】`));
|
||||
secondLevelResources.forEach((res, idx) => log.info(` ${idx + 1}. 【${res.dirName}】`));
|
||||
log.info("=============================================================\n");
|
||||
|
||||
// 核心逻辑:外循环遍历所有一级弹窗
|
||||
@@ -253,12 +252,11 @@ async function imageClickBackgroundTask() {
|
||||
|
||||
// 遍历所有一级弹窗
|
||||
for (const currentFirstLevel of firstLevelDirs) {
|
||||
log.info(`【${currentFirstLevel.dirName}】准备识别...`);
|
||||
// 检查当前一级弹窗是否被触发
|
||||
const levelResult = await imageClick([currentFirstLevel], null, [currentFirstLevel.dirName], true);
|
||||
|
||||
if (levelResult.success) {
|
||||
// log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
|
||||
log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
|
||||
const levelStack = [currentFirstLevel];
|
||||
|
||||
// 内循环处理内部流程
|
||||
@@ -304,7 +302,7 @@ async function imageClickBackgroundTask() {
|
||||
// log.info(`===== 外循环结束:等待${taskDelay/1000}秒后开始下一次循环 =====`);
|
||||
await sleep(taskDelay);
|
||||
}
|
||||
|
||||
|
||||
log.info("imageClick后台任务结束");
|
||||
return { success: true };
|
||||
}
|
||||
@@ -325,14 +323,14 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
for (const foundRegion of foundRegions) {
|
||||
const tolerance = 1;
|
||||
const iconMat = file.readImageMatSync(`${normalizePath(foundRegion.iconDir)}/${foundRegion.iconName}`);
|
||||
|
||||
|
||||
const { detectRegion } = popupConfig;
|
||||
const defaultX = foundRegion.region.x - tolerance;
|
||||
const defaultY = foundRegion.region.y - tolerance;
|
||||
const defaultWidth = foundRegion.region.width + 2 * tolerance;
|
||||
const defaultHeight = foundRegion.region.height + 2 * tolerance;
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(
|
||||
iconMat,
|
||||
iconMat,
|
||||
detectRegion?.x ?? defaultX,
|
||||
detectRegion?.y ?? defaultY,
|
||||
detectRegion?.width ?? defaultWidth,
|
||||
@@ -349,7 +347,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
useNewScreenshot,
|
||||
dirName
|
||||
);
|
||||
|
||||
|
||||
if (result.isDetected && result.x !== 0 && result.y !== 0) {
|
||||
hasAnyIconDetected = true;
|
||||
isAnySuccess = true;
|
||||
@@ -360,12 +358,12 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
log.info(`识别到【${dirName}】弹窗,偏移后位置(${actualX}, ${actualY})`);
|
||||
|
||||
if (!popupConfig.isSpecial) {
|
||||
// log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})`);
|
||||
// 新增:普通点击加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const clickCount = popupConfig.loopCount;
|
||||
const clickDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < clickCount; i++) {
|
||||
await click(actualX, actualY); // 保留原始点击逻辑
|
||||
// log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}次`);
|
||||
if (i < clickCount - 1) await sleep(clickDelay); // 非最后一次加间隔
|
||||
}
|
||||
} else {
|
||||
@@ -374,10 +372,10 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
const targetKey = popupConfig.keyCode || "VK_SPACE";
|
||||
// 新增:key_press用循环(默认3次,1000ms间隔,与原硬编码逻辑一致)
|
||||
const pressCount = popupConfig.loopCount || 3;
|
||||
const pressDelay = popupConfig.loopDelay || 500;
|
||||
const pressDelay = popupConfig.loopDelay || 1000;
|
||||
for (let i = 0; i < pressCount; i++) {
|
||||
keyPress(targetKey); // 保留原始按键逻辑
|
||||
log.info(`【${dirName}】弹窗触发按键【${targetKey}】${i+1}次`);
|
||||
log.info(`【${dirName}】弹窗触发按键【${targetKey}】${i + 1}次`);
|
||||
if (i < pressCount - 1) await sleep(pressDelay); // 非最后一次加间隔
|
||||
}
|
||||
log.info(`【${dirName}】弹窗触发按键【${targetKey}】,共${pressCount}次,间隔${pressDelay}ms`);
|
||||
@@ -392,8 +390,8 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
}
|
||||
const ocrResults = await performOcr(targetTexts, xRange, yRange, timeout, ra);
|
||||
if (ocrResults.length > 0) {
|
||||
const ocrActualX = Math.round(ocrResults[0].x + ocrResults[0].width/2) + xOffset;
|
||||
const ocrActualY = Math.round(ocrResults[0].y + ocrResults[0].height/2) + yOffset;
|
||||
const ocrActualX = Math.round(ocrResults[0].x + ocrResults[0].width / 2) + xOffset;
|
||||
const ocrActualY = Math.round(ocrResults[0].y + ocrResults[0].height / 2) + yOffset;
|
||||
// 新增:OCR点击加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const ocrCount = popupConfig.loopCount;
|
||||
const ocrDelay = popupConfig.loopDelay;
|
||||
@@ -443,7 +441,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
|
||||
const defaultDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < defaultCount; i++) {
|
||||
await click(actualX, actualY); // 保留原始默认点击逻辑
|
||||
log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}次`);
|
||||
log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i + 1}次`);
|
||||
if (i < defaultCount - 1) await sleep(defaultDelay); // 非最后一次加间隔
|
||||
}
|
||||
isAnySuccess = true;
|
||||
|
||||
@@ -1,122 +1,124 @@
|
||||
// 定义替换映射表
|
||||
const replacementMap = {
|
||||
"监": "盐",
|
||||
"炽": "烬",
|
||||
"盞": "盏",
|
||||
"攜": "携",
|
||||
"於": "于",
|
||||
"卵": "卯"
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 执行OCR识别并匹配目标文本(失败自动重截图,返回结果+有效截图)
|
||||
* @param {string[]} targetTexts - 待匹配的目标文本列表
|
||||
* @param {Object} xRange - X轴范围 { min: number, max: number }
|
||||
* @param {Object} yRange - Y轴范围 { min: number, max: number }
|
||||
* @param {Object} ra - 初始图像捕获对象(外部传入,需确保已初始化)
|
||||
* @param {number} timeout - 超时时间(毫秒),默认200ms
|
||||
* @param {number} interval - 重试间隔(毫秒),默认50ms
|
||||
* @returns {Promise<{
|
||||
* results: Object[], // 识别结果数组(含文本、坐标)
|
||||
* screenshot: Object // 有效截图(成功时用的截图/超时前最后一次截图)
|
||||
* }>}
|
||||
*/
|
||||
async function performOcr(targetTexts, xRange, yRange, ra = null, timeout = 200, interval = 50) {
|
||||
// 正则特殊字符转义工具函数(避免替换时的正则语法错误)
|
||||
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const startTime = Date.now(); // 记录开始时间,用于超时判断
|
||||
let currentScreenshot = ra; // 跟踪当前有效截图(初始为外部传入的原图)
|
||||
|
||||
// 1. 初始参数校验(提前拦截无效输入)
|
||||
if (!currentScreenshot) {
|
||||
throw new Error("初始图像捕获对象(ra)未初始化,请传入有效截图");
|
||||
}
|
||||
const regionWidth = xRange.max - xRange.min;
|
||||
const regionHeight = yRange.max - yRange.min;
|
||||
if (regionWidth <= 0 || regionHeight <= 0) {
|
||||
throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`);
|
||||
}
|
||||
|
||||
// 在超时时间内循环重试识别(处理临时失败,自动重截图)
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// 额外增加空值检查,防止currentScreenshot变为null
|
||||
if (!currentScreenshot) {
|
||||
log.error("currentScreenshot为null,尝试重新捕获");
|
||||
currentScreenshot = captureGameRegion();
|
||||
await sleep(interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 执行OCR识别(基于当前有效截图的指定区域)
|
||||
const resList = currentScreenshot.findMulti(
|
||||
RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight)
|
||||
);
|
||||
|
||||
// 3. 处理识别结果(文本修正 + 目标匹配)
|
||||
const results = [];
|
||||
for (let i = 0; i < resList.count; i++) {
|
||||
const res = resList[i];
|
||||
let correctedText = res.text; // 修正后的文本
|
||||
const originalText = res.text; // 保留原始识别文本(便于调试)
|
||||
log.debug(`OCR原始文本: ${res.text}`);
|
||||
|
||||
// 3.1 修正OCR常见错误(基于替换映射表)
|
||||
for (const [wrongChar, correctChar] of Object.entries(replacementMap)) {
|
||||
const escapedWrong = escapeRegExp(wrongChar);
|
||||
correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar);
|
||||
}
|
||||
|
||||
// 3.2 匹配目标文本(避免重复添加同一结果)
|
||||
const isTargetMatched = targetTexts.some(target => correctedText.includes(target));
|
||||
if (isTargetMatched) {
|
||||
results.push({
|
||||
text: correctedText, // 最终修正后的文本
|
||||
originalText: originalText, // 原始识别文本(调试用)
|
||||
x: res.x, y: res.y, // 文本在截图中的X/Y坐标
|
||||
width: res.width, height: res.height // 文本区域尺寸
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 识别成功:返回「结果数组 + 本次成功用的截图」
|
||||
// log.info(`OCR识别完成,匹配到${results.length}个目标文本`);
|
||||
return {
|
||||
results: results,
|
||||
screenshot: currentScreenshot // 成功截图(与结果对应的有效画面)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
// 5. 识别失败:释放旧截图→重新捕获→更新当前截图
|
||||
if (currentScreenshot) {
|
||||
// 检查是否存在释放方法,支持不同可能的命名
|
||||
if (typeof currentScreenshot.Dispose === 'function') {
|
||||
currentScreenshot.Dispose();
|
||||
} else if (typeof currentScreenshot.dispose === 'function') {
|
||||
currentScreenshot.dispose();
|
||||
}
|
||||
log.debug("已释放旧截图资源,准备重新捕获");
|
||||
}
|
||||
|
||||
// 重新捕获后增加null校验
|
||||
currentScreenshot = captureGameRegion();
|
||||
if (!currentScreenshot) {
|
||||
log.error("重新捕获截图失败,返回了null值");
|
||||
}
|
||||
|
||||
log.error(`OCR识别异常(已重新截图,将重试): ${error.message}`);
|
||||
await sleep(5); // 短暂等待,避免高频截图占用CPU/内存
|
||||
}
|
||||
|
||||
await sleep(interval); // 每次重试间隔(默认50ms)
|
||||
}
|
||||
|
||||
// 6. 超时未成功:返回「空结果 + 超时前最后一次截图」
|
||||
log.warn(`OCR识别超时(超过${timeout}ms)`);
|
||||
return {
|
||||
results: [],
|
||||
screenshot: currentScreenshot // 超时前最后一次有效截图(可用于排查原因)
|
||||
};
|
||||
}
|
||||
// 定义替换映射表
|
||||
const replacementMap = {
|
||||
"监": "盐",
|
||||
"炽": "烬",
|
||||
"盞": "盏",
|
||||
"攜": "携",
|
||||
"於": "于",
|
||||
"卵": "卯"
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 执行OCR识别并匹配目标文本(失败自动重截图,返回结果+有效截图)
|
||||
* @param {string[]} targetTexts - 待匹配的目标文本列表
|
||||
* @param {Object} xRange - X轴范围 { min: number, max: number }
|
||||
* @param {Object} yRange - Y轴范围 { min: number, max: number }
|
||||
* @param {Object} ra - 初始图像捕获对象(外部传入,需确保已初始化)
|
||||
* @param {number} timeout - 超时时间(毫秒),默认200ms
|
||||
* @param {number} interval - 重试间隔(毫秒),默认50ms
|
||||
* @returns {Promise<{
|
||||
* results: Object[], // 识别结果数组(含文本、坐标)
|
||||
* screenshot: Object // 有效截图(成功时用的截图/超时前最后一次截图)
|
||||
* }>}
|
||||
*/
|
||||
async function performOcr(targetTexts, xRange, yRange, ra = null, timeout = 200, interval = 50) {
|
||||
// 正则特殊字符转义工具函数(避免替换时的正则语法错误)
|
||||
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const startTime = Date.now(); // 记录开始时间,用于超时判断
|
||||
let currentScreenshot = ra; // 跟踪当前有效截图(初始为外部传入的原图)
|
||||
|
||||
// 1. 初始参数校验(提前拦截无效输入)
|
||||
if (!currentScreenshot) {
|
||||
throw new Error("初始图像捕获对象(ra)未初始化,请传入有效截图");
|
||||
}
|
||||
const regionWidth = xRange.max - xRange.min;
|
||||
const regionHeight = yRange.max - yRange.min;
|
||||
if (regionWidth <= 0 || regionHeight <= 0) {
|
||||
throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`);
|
||||
}
|
||||
|
||||
// 在超时时间内循环重试识别(处理临时失败,自动重截图)
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// 额外增加空值检查,防止currentScreenshot变为null
|
||||
if (!currentScreenshot) {
|
||||
log.error("currentScreenshot为null,尝试重新捕获");
|
||||
currentScreenshot = captureGameRegion();
|
||||
await sleep(interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 执行OCR识别(基于当前有效截图的指定区域)
|
||||
const resList = currentScreenshot.findMulti(
|
||||
RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight)
|
||||
);
|
||||
|
||||
// 3. 处理识别结果(文本修正 + 目标匹配)
|
||||
const results = [];
|
||||
for (let i = 0; i < resList.count; i++) {
|
||||
const res = resList[i];
|
||||
let correctedText = res.text; // 修正后的文本
|
||||
const originalText = res.text; // 保留原始识别文本(便于调试)
|
||||
log.debug(`OCR原始文本: ${res.text}`);
|
||||
|
||||
// 3.1 修正OCR常见错误(基于替换映射表)
|
||||
for (const [wrongChar, correctChar] of Object.entries(replacementMap)) {
|
||||
const escapedWrong = escapeRegExp(wrongChar);
|
||||
correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar);
|
||||
}
|
||||
|
||||
// 3.2 匹配目标文本(双向匹配,避免重复添加同一结果)
|
||||
const isTargetMatched = targetTexts.some(target =>
|
||||
correctedText.includes(target) || target.includes(correctedText)
|
||||
);
|
||||
if (isTargetMatched) {
|
||||
results.push({
|
||||
text: correctedText, // 最终修正后的文本
|
||||
originalText: originalText, // 原始识别文本(调试用)
|
||||
x: res.x, y: res.y, // 文本在截图中的X/Y坐标
|
||||
width: res.width, height: res.height // 文本区域尺寸
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 识别成功:返回「结果数组 + 本次成功用的截图」
|
||||
// log.info(`OCR识别完成,匹配到${results.length}个目标文本`);
|
||||
return {
|
||||
results: results,
|
||||
screenshot: currentScreenshot // 成功截图(与结果对应的有效画面)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
// 5. 识别失败:释放旧截图→重新捕获→更新当前截图
|
||||
if (currentScreenshot) {
|
||||
// 检查是否存在释放方法,支持不同可能的命名
|
||||
if (typeof currentScreenshot.Dispose === 'function') {
|
||||
currentScreenshot.Dispose();
|
||||
} else if (typeof currentScreenshot.dispose === 'function') {
|
||||
currentScreenshot.dispose();
|
||||
}
|
||||
log.debug("已释放旧截图资源,准备重新捕获");
|
||||
}
|
||||
|
||||
// 重新捕获后增加null校验
|
||||
currentScreenshot = captureGameRegion();
|
||||
if (!currentScreenshot) {
|
||||
log.error("重新捕获截图失败,返回了null值");
|
||||
}
|
||||
|
||||
log.error(`OCR识别异常(已重新截图,将重试): ${error.message}`);
|
||||
await sleep(5); // 短暂等待,避免高频截图占用CPU/内存
|
||||
}
|
||||
|
||||
await sleep(interval); // 每次重试间隔(默认50ms)
|
||||
}
|
||||
|
||||
// 6. 超时未成功:返回「空结果 + 超时前最后一次截图」
|
||||
log.warn(`OCR识别超时(超过${timeout}ms)`);
|
||||
return {
|
||||
results: [],
|
||||
screenshot: currentScreenshot // 超时前最后一次有效截图(可用于排查原因)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
var globalLatestRa = null;
|
||||
async function recognizeImage(
|
||||
recognitionObject,
|
||||
ra,
|
||||
timeout = 1000,
|
||||
interval = 500,
|
||||
useNewScreenshot = false,
|
||||
recognitionObject,
|
||||
ra,
|
||||
timeout = 1000,
|
||||
interval = 500,
|
||||
useNewScreenshot = false,
|
||||
iconType = null
|
||||
) {
|
||||
let startTime = Date.now();
|
||||
globalLatestRa = ra;
|
||||
const originalRa = ra;
|
||||
globalLatestRa = ra;
|
||||
const originalRa = ra;
|
||||
let tempRa = null; // 用于管理临时创建的资源
|
||||
|
||||
try {
|
||||
@@ -73,3 +73,79 @@ async function recognizeImage(
|
||||
usedNewScreenshot: useNewScreenshot
|
||||
};
|
||||
}
|
||||
|
||||
// 定义一个异步函数来绘制红框并延时清除
|
||||
async function drawAndClearRedBox(searchRegion, ra, delay = 500) {
|
||||
let drawRegion = null;
|
||||
try {
|
||||
// 创建绘制区域
|
||||
drawRegion = ra.DeriveCrop(
|
||||
searchRegion.x, searchRegion.y,
|
||||
searchRegion.width, searchRegion.height
|
||||
);
|
||||
drawRegion.DrawSelf("icon"); // 绘制红框
|
||||
|
||||
// 等待指定时间
|
||||
await sleep(delay);
|
||||
|
||||
// 清除红框 - 使用更可靠的方式
|
||||
if (drawRegion && typeof drawRegion.DrawSelf === 'function') {
|
||||
// 可能需要使用透明绘制来清除,或者绘制一个0大小的区域
|
||||
ra.DeriveCrop(0, 0, 0, 0).DrawSelf("icon");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("红框绘制异常:" + e.message);
|
||||
} finally {
|
||||
// 正确释放资源,如果dispose方法存在的话
|
||||
if (drawRegion && typeof drawRegion.dispose === 'function') {
|
||||
drawRegion.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截图保存函数
|
||||
function imageSaver(mat, saveFile) {
|
||||
// 获取当前时间并格式化为 "YYYY-MM-DD_HH-MM-SS"
|
||||
const now = new Date();
|
||||
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
// 获取当前脚本所在的目录
|
||||
const scriptDir = getScriptDirPath();
|
||||
if (!scriptDir) {
|
||||
log.error("无法获取脚本目录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建完整的目标目录路径和文件名
|
||||
const savePath = `${scriptDir}/${saveFile}/screenshot_${timestamp}.png`;
|
||||
const tempFilePath = `${scriptDir}/${saveFile}`;
|
||||
|
||||
// 检查临时文件是否存在,如果不存在则创建目录
|
||||
try {
|
||||
// 尝试读取临时文件
|
||||
file.readPathSync(tempFilePath);
|
||||
log.info("目录存在,继续执行保存图像操作");
|
||||
} catch (error) {
|
||||
log.error(`确保目录存在时出错: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存图像
|
||||
try {
|
||||
mat.saveImage(savePath);
|
||||
// log.info(`图像已成功保存到: ${savePath}`);
|
||||
} catch (error) {
|
||||
log.error(`保存图像失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取脚本目录
|
||||
function getScriptDirPath() {
|
||||
try {
|
||||
safeReadTextSync(`temp-${Math.random()}.txt`);
|
||||
} catch (e) {
|
||||
const match = e.toString().match(/'([^']+)'/);
|
||||
return match ? match[1].replace(/\\[^\\]+$/, "") : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "背包统计采集系统",
|
||||
"version": "2.55",
|
||||
"bgi_version": "0.44.8",
|
||||
"name": "背包材料统计及采集管理系统",
|
||||
"version": "2.60",
|
||||
"bgi_version": "0.55",
|
||||
"description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md",
|
||||
"saved_files": [
|
||||
"pathing/",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "TargetCount",
|
||||
"type": "input-text",
|
||||
"label": "js目录下默认扫描的3层文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
"label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
},
|
||||
{
|
||||
"name": "TargetresourceName",
|
||||
@@ -29,70 +29,33 @@
|
||||
"type": "select",
|
||||
"label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:仅📁pathing材料",
|
||||
"options": [
|
||||
"1.兼并:📁pathing材料+【材料分类】",
|
||||
"2.仅📁pathing材料",
|
||||
"3.仅【材料分类】勾选",
|
||||
]
|
||||
"1.兼并:📁pathing材料+【材料分类】",
|
||||
"2.仅📁pathing材料",
|
||||
"3.仅【材料分类】勾选"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Smithing",
|
||||
"type": "checkbox",
|
||||
"label": "\n----------------------------------\n【锻造素材】"
|
||||
},
|
||||
{
|
||||
"name": "Drops",
|
||||
"type": "checkbox",
|
||||
"label": "如:矿石、原胚\n----------------------------------\n【怪物掉落素材】"
|
||||
},
|
||||
{
|
||||
"name": "ForagedFood",
|
||||
"type": "checkbox",
|
||||
"label": "如:经验书、怪物掉落\n----------------------------------\n【采集食物】,食用回血"
|
||||
},
|
||||
{
|
||||
"name": "General",
|
||||
"type": "checkbox",
|
||||
"label": "如:苹果、日落果、泡泡桔\n----------------------------------\n【一般素材】"
|
||||
},
|
||||
{
|
||||
"name": "CookingIngs",
|
||||
"type": "checkbox",
|
||||
"label": "如:特产、非食用素材\n----------------------------------\n\n【烹饪用食材】"
|
||||
},
|
||||
{
|
||||
"name": "Weekly",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【周本素材】"
|
||||
},
|
||||
{
|
||||
"name": "Wood",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【木材】"
|
||||
},
|
||||
{
|
||||
"name": "CharAscension",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【角色培养素材】世界BOSS树脂材料"
|
||||
},
|
||||
{
|
||||
"name": "Fishing",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【鱼饵、鱼类】"
|
||||
},
|
||||
{
|
||||
"name": "Gems",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【宝石】"
|
||||
},
|
||||
{
|
||||
"name": "Talent",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【角色天赋素材】天赋书"
|
||||
},
|
||||
{
|
||||
"name": "WeaponAscension",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【武器突破素材】"
|
||||
"name": "Categories",
|
||||
"type": "multi-checkbox",
|
||||
"label": "\n----------------------------------\n【材料分类】",
|
||||
"options": [
|
||||
"矿石、原胚",
|
||||
"经验书、怪物掉落",
|
||||
"采集食物",
|
||||
"一般素材",
|
||||
"烹饪用食材",
|
||||
"周本素材",
|
||||
"木材",
|
||||
"世界BOSS",
|
||||
"鱼饵、鱼类",
|
||||
"宝石",
|
||||
"天赋素材",
|
||||
"武器突破"
|
||||
],
|
||||
"default": [
|
||||
"一般素材",
|
||||
"烹饪用食材"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PopupNames",
|
||||
@@ -102,12 +65,12 @@
|
||||
{
|
||||
"name": "PopupClickDelay",
|
||||
"type": "input-text",
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意文件夹结构\n----------------------------------\n弹窗循环间隔(默认:15 秒)"
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)"
|
||||
},
|
||||
{
|
||||
"name": "CDCategories",
|
||||
"type": "input-text",
|
||||
"label": "====================\n\n采用的CD分类(默认:全部) 举例:采集,怪物,木材"
|
||||
"label": "----------------------------------\n\n采用的CD分类(默认:全部) 举例:采集,怪物,木材"
|
||||
},
|
||||
{
|
||||
"name": "CurrentTime",
|
||||
@@ -122,12 +85,7 @@
|
||||
{
|
||||
"name": "ExceedCount",
|
||||
"type": "input-text",
|
||||
"label": "根据拾取分类来加载OCR名单,具体看targetText目录\n----------------------------------\n\n超量阈值(默认:5000)超量的路径材料将不拾取"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "====================\n拖动距离:(默认711像素点)"
|
||||
"label": "举例:交互,采集,宝箱\n----------------------------------\n超量阈值(默认:9000)超量的路径材料将不拾取"
|
||||
},
|
||||
{
|
||||
"name": "HoldX",
|
||||
@@ -138,5 +96,10 @@
|
||||
"name": "HoldY",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n拖动距离:(默认711像素点)"
|
||||
}
|
||||
]
|
||||
]
|
||||
Reference in New Issue
Block a user