背包采集统计优化修复 (#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:
吉吉喵
2026-02-10 11:39:53 +08:00
committed by GitHub
parent b83d40c09a
commit 5337cecfdf
10 changed files with 1413 additions and 707 deletions

View File

@@ -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.txt0收获记录
- `pathing_record`:单个路径的完整记录(含运行时间、收获量,需重点备份),材料收集汇总.txt始末差值记录标准名-0.txt0收获记录,三次及以上同名路径记录,就会触发排除
操作参考截图:
<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报错 |

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 // 超时前最后一次有效截图(可用于排查原因)
};
}

View File

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

View File

@@ -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/",

View File

@@ -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像素点)"
}
]
]