新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 (#2174)

* Add files via upload

* Add files via upload

* Add files via upload

* 添加日志

* Add files via upload

* Add files via upload

* Add files via upload

---------

Co-authored-by: zaodonganqi <3193156071@qq.com>
This commit is contained in:
吉吉喵
2025-10-18 21:05:28 +08:00
committed by GitHub
parent e2add1c5d4
commit f3edc73809
22 changed files with 512 additions and 166 deletions

View File

@@ -38,14 +38,15 @@
### 核心优势
1. **自动CD判断**无需手动关注材料刷新状态脚本自动识别CD是否就绪
2. **灵活路径管理**:支持自定义添加路径,自动排除低效/无效路径;
3. **独立名单识别**不与路边NPC、神像交互可自定义识别名单操作见「四、问题解答」
3. **独立名单识别**不与路边NPC、神像交互可自定义识别名单操作见「四、问题解答Q4」);
4. **实时弹窗保护**:内置弹窗模块(覆盖路边信件、过期物品、月卡、调查等场景),运行时全程保护路径不被弹窗干扰。
5. **自动黑名单**:内置拾取模块,联动材料统计,可识别爆满的路径材料,自动屏蔽。
## 二、用前须知
1. 需具备基础电脑操作能力(如文件夹复制、路径查找);
2. 脚本不自带路径文件,需手动对目标文件夹进行操作(步骤见「三、使用方法」)。
2. 非1080p显示器使用前需要根据显示器调整背包物品界面的 拖动距离 推荐“一次划页稍小于4行材料的距离”
3. 脚本不自带路径文件,需手动对目标文件夹进行操作(步骤见「三、使用方法」)。
## 三、使用方法
@@ -110,14 +111,16 @@
| 配置项 | 功能说明 | 操作建议 |
|----------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------|
| 1. 目标数量 | 仅当背包材料数量**低于此值**时,该材料的路径才会被纳入执行序列 | 这是个统一值,管理路径下全部材料的目标数量 |
| 2. 优先级材料 | 无视“目标数量”限制,直接纳入执行序列顶层(最高优先级) | 填写当前急需材料(例:虹滴晶”“巡陆艇 |
| 2. 优先级材料 | 无视“目标数量”限制,直接纳入执行序列顶层(最高优先级) | 填写当前急需材料(例:虹滴晶,巡陆艇) |
| 3. 时间成本 | 当一个路径有3-5次运行记录后自动计算“单材料获取时间”超过30秒则跳过该路径 | 保持默认30秒即可无需频繁修改可过滤低效路径 |
| 4. 发送通知 | ① 每类材料跑完通知一次;② 全部材料跑完汇总通知一次需开启BGI通知 | 建议开启,方便实时了解进度(接收端如企业微信需自行配置) |
| 5. 取消扫描 | 取消“每个路径执行后”的背包扫描,仅保留“全部执行前/后”2次扫描 | 有效路径记录达3条以上时可以开启可节约运行时间 |
| 6. 仅 pathing 材料 | 仅扫描 `pathing` 文件夹内的材料,跳过其他分类,大幅缩短扫描时间 | 路径配置完成后开启,提升脚本运行效率 |
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:仅处理月卡弹窗则填:月卡) |
| 8. 采用的 CD 分类 | 执行 `materialsCD` 文件夹内配置的材料路径支持新增CD分类txt | 新增材料时需在该文件夹同步配置CD规则操作见「四、问题解答4」) |
| 9. 拖动距离 | 解决非1080p分辨率下“划页过头”问题需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”大于1080p屏可适当减小数值 |
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏 |
| 8. 采用的 CD 分类 | 不填则默认执行 `materialsCD` 文件夹内配置的CD分类填写则仅执行指定CD分类 | 新增材料时需在该文件夹同步配置CD规则操作见「四、问题解答Q2」) |
| 9. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时需符合配置规则操作见「四、问题解答Q4」 |
| 10. 超量阈值 | 首次扫描后超量的路径材料将从识别名单中剔除默认5000 | 不推荐9999怪物材料有几千就够了采用默认数值可自动避免爆背包 |
| 11. 拖动距离 | 解决非1080p分辨率下“划页过头”问题需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”大于1080p屏可适当减小数值 |
## 四、注意事项
@@ -134,23 +137,9 @@
### Q1如何排除不想要的路径
A1. 打开 `pathing` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\pathing`
2. 直接删除/移走目标材料/怪物的路径文件夹;
3. **注意**:不要将路径文件放入 `targetText` 或 `materialsCD` 文件夹(① 这两个文件夹默认全部读取,增加负担;② 也不安全,会被更新覆盖)
**其他方法**看「四、问题解答Q2」按格式要求填入对应的材料名或者从其他CD文件中复制过来在「JS 自定义设置」【采用的 CD 分类】中输入新建的文件名即可实现只加载该CD文件里材料的路径
### Q2如何自定义识别名单
A1. 打开 `targetText` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\targetText`
2. 新建/编辑txt文件按格式填写`自定义名称:目标1,目标2`(英文冒号+英文逗号,例:“新材料:霜盏花,便携轴承,”);
3. 若需排除怪物掉落材料:找到“掉落.txt”删除对应材料名即可
4. 操作参考截图:
<div class="img-row-container">
<img src="assets/Pic/Pic12.png" alt="自定义识别名单操作截图1" class="img-row-item">
<img src="assets/Pic/Pic10.png" alt="自定义识别名单操作截图2" class="img-row-item">
</div>
### Q3如何识别不规范命名的路径文件夹如“纳塔食材一条龙”“果园.json”
A将不规范的文件夹/文件,放入**适配的材料文件夹**中即可路径CD由“所在材料文件夹”决定
例:“果园.json”放入“苹果”文件夹将按“苹果”的CD规则执行。
### Q4如何添加新材料
### Q2如何添加新材料
A1. 打开 `materialsCD` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\materialsCD`
2. 新建/编辑txt文件按格式填写`CD规则材料1材料2`(中文冒号+中文逗号CD规则参考自带文件“1次0点月落银宿影花`材料1`或`材料2`将会作为标准名;
3. **关键要求**路径文件夹名、材料图片名必须与“材料1或2”完全一致多层文件夹默认读取最外层标准名文件夹
@@ -160,6 +149,20 @@ A1. 打开 `materialsCD` 文件夹(脚本路径:`BetterGI\User\JsScript\
<img src="assets/Pic/Pic08.png" alt="添加新材料操作截图2" class="img-row-item">
</div>
### Q3如何识别不规范命名的路径文件夹如“纳塔食材一条龙”“果园.json”
A将不规范的文件夹/文件,放入**适配的材料文件夹**中即可路径CD由“所在材料文件夹”决定
例:“果园.json”放入“苹果”文件夹将按“苹果”的CD规则执行。
### Q4如何自定义识别名单
A1. 打开 `targetText` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\targetText`
2. 新建/编辑txt文件按格式填写`自定义名称:目标1,目标2`(英文冒号+英文逗号,例:“新材料:霜盏花,便携轴承,”);
3. 若需排除怪物掉落材料:找到“掉落.txt”删除对应材料名即可
4. 操作参考截图:
<div class="img-row-container">
<img src="assets/Pic/Pic12.png" alt="自定义识别名单操作截图1" class="img-row-item">
<img src="assets/Pic/Pic10.png" alt="自定义识别名单操作截图2" class="img-row-item">
</div>
### Q5如何取消路径执行后扫描背包
A在「JS自定义设置」中勾选“取消扫描”依旧会保留“全部材料执行始/末”的2次扫描
@@ -209,4 +212,5 @@ A记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
| v2.41 | 修复“勾选分类的本地记录”bug新增“仅背包统计”选项补充记录损坏处理说明 |
| v2.42 | 新增“无路径间扫描”“noRecord模式”适合成熟路径新增怪物材料CD文件 |
| v2.50 | 新增独立名单拾取、弹窗模块;支持怪物名识别 |
| v2.51 | 自定义设置新增“拖动距离/拖动点”新增月卡弹窗识别路径材料达9999自动上黑名单修复怪物0收获记录 |
| v2.51 | 自定义设置新增“拖动距离/拖动点”新增月卡弹窗识别路径材料达9999自动上黑名单修复怪物0收获记录 |
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |

View File

@@ -0,0 +1,10 @@
{
"isSpecial": true,
"operationType": "key_press",
"keyCode": "VK_F",
"xOffset": 0,
"yOffset": 0,
"detectRegion": { "x": 1207, "y": 783, "width": 42, "height": 42 },
"nextLevelOnSuccess": "",
"nextLevelOnFailure": ""
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

View File

@@ -0,0 +1,10 @@
{
"isSpecial": true,
"operationType": "key_mouse_script",
"scriptPath": "assets/滚轮下翻.json",
"xOffset": 0,
"yOffset": 0,
"detectRegion": { "x": 1206, "y": 175, "width": 44, "height": 735 },
"nextLevelOnSuccess": "assets\\imageClick\\其他\\F",
"nextLevelOnFailure": ""
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,15 @@
{
"isSpecial": true,
"operationType": "key_press",
"keyCode": "VK_SPACE",
"xOffset": 950,
"yOffset": 1050,
"detectRegion": {
"x": 264,
"y": 36,
"width": 66,
"height": 24
},
"nextLevelOnSuccess": "assets\\imageClick\\其他\\滚轮",
"nextLevelOnFailure": ""
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,6 @@
{
"xOffset": 0,
"yOffset": 455,
"loopCount": 3,
"loopDelay": 1000
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
{ "xOffset": -845, "yOffset": 325 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -31,18 +31,31 @@ function readtargetTextCategories(targetTextDir) {
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1);
const materialCategories = {};
// 解析筛选名单
const pickTextNames = (settings.PickCategories || "")
.split(/[,,、 \s]+/).map(n => n.trim()).filter(n => n);
// 【新增兜底日志】确认pickTextNames是否为空方便排查
log.info(`筛选名单状态:${pickTextNames.length === 0 ? '未指定(空),将加载所有文件' : '指定了:' + pickTextNames.join(',')}`);
for (const filePath of targetTextFilePaths) {
if (state.cancelRequested) break;
const content = file.readTextSync(filePath);
if (!content) {
log.error(`加载文件失败:${filePath}`);
continue; // 跳过当前文件
continue;
}
const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名
// 【核心筛选:空名单直接跳过判断,加载所有】
if (pickTextNames.length === 0) {
// 空名单时,直接保留当前文件,不跳过
} else if (!pickTextNames.includes(sourceCategory)) {
// 非空名单且不在列表里,才跳过
continue;
}
materialCategories[sourceCategory] = parseCategoryContent(content);
}
// log.info(`完成材料分类信息读取,分类信息:${JSON.stringify(materialCategories, null, 2)}`);
return materialCategories;
}
// 定义替换映射表
@@ -171,7 +184,7 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
log.info("检测中...");
lastLogTime = currentTime;
}
await sleep(50); // 关键50时可避免F多目标滚动中拾取错背包js这边有弹窗模块就没必要增加延迟降低效率了
await sleep(50);
cachedFrame?.dispose();
cachedFrame = captureGameRegion();
@@ -192,7 +205,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
for (let targetText of targetTexts) {
let targetResult = ocrResults.find(res => res.text.includes(targetText));
if (targetResult) {
// log.info(`找到目标文本: ${targetText}`);
// 生成唯一标识并更新识别计数(文本+Y坐标
const materialId = `${targetText}-${targetResult.y}`;
@@ -203,7 +215,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
// log.info(`目标文本 '${targetText}' 和 F 图标水平对齐`);
if (recognitionCount.get(materialId) >= 1) {
keyPress("F"); // 执行交互操作
// log.info(`F键执行成功识别计数: ${recognitionCount.get(materialId)}`);
log.info(`交互或拾取: ${targetText}`);
// F键后清除计数确保单次交互

View File

@@ -4,6 +4,7 @@ const holdX = Math.min(1920, Math.max(0, Math.floor(Number(settings.HoldX) || 10
const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 750)));
const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711));
const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量
const exceedCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.ExceedCount) || 5000))); // 设定的超量目标数量
const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay);
// 配置参数
@@ -383,7 +384,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance
};
const ocrResult = await recognizeText(ocrRegion, 1000, 10, 5, 2, ra);
const ocrResult = await recognizeText(ocrRegion, 200, 10, 5, 2, ra);
materialInfo.push({ name, count: ocrResult || "?" });
if (!hasFoundFirstMaterial) {
@@ -516,7 +517,7 @@ var excessMaterialNames = []; // 超量材料名单
function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) {
// 新增超量阈值普通材料9999矿石处理后也是9999
const EXCESS_THRESHOLD = 9999;
const EXCESS_THRESHOLD = exceedCount;
// 新增:临时存储本次超量材料
const tempExcess = [];

View File

@@ -1,16 +1,20 @@
// ======================== 全局工具函数只定义1次所有函数共用========================
// 1. 路径标准化函数(统一处理,消除重复)
function normalizePath(path) {
if (!path || typeof path !== 'string') return '';
let standardPath = path.replace(/\\/g, '/').replace(/\/+/g, '/');
return standardPath.endsWith('/') ? standardPath.slice(0, -1) : standardPath;
}
// ==============================================
// 5. 角色识别与策略执行相关函数(保留原始功能)
// ==============================================
// 工具函数
// 2. 提取路径最后一级名称
function basename(filePath) {
if (!filePath || typeof filePath !== 'string') return '';
const normalizedPath = filePath.replace(/\\/g, '/');
const normalizedPath = normalizePath(filePath);
const lastSlashIndex = normalizedPath.lastIndexOf('/');
return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
}
/*
// 如果路径存在且返回的是数组则认为是目录Folder
function pathExists(path) {

View File

@@ -1,195 +1,457 @@
// 新增:独立的预加载函数,负责所有资源预处理
// ======================== 1. 预加载函数(精简日志版)========================
async function preloadImageResources(specificNames) {
log.info("开始预加载所有图片资源");
// log.info("开始预加载所有图片资源");
function hasIconFolder(dirPath) {
try {
const entries = readAllFilePaths(dirPath, 0, 0, [], true);
return entries.some(entry => normalizePath(entry).endsWith('/icon'));
} catch (e) {
log.error(`检查目录【${dirPath}】是否有icon文件夹失败${e.message}`);
return false;
}
}
// 统一参数格式(与原逻辑一致)
let preSpecificNames = specificNames;
if (typeof specificNames === 'string') {
preSpecificNames = [specificNames];
}
if (typeof specificNames === 'string') preSpecificNames = [specificNames];
const isAll = !preSpecificNames || preSpecificNames.length === 0;
if (isAll) {
log.info("未指定具体弹窗名称,将执行所有弹窗目录处理");
} else {
log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}`);
}
// if (isAll) log.info("未指定具体弹窗名称将执行所有含icon文件夹的弹窗目录处理");
// else log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}仅含icon文件夹的目录`);
// 定义根目录(与原代码一致)
const rootDir = "assets/imageClick";
const rootDirNormalized = normalizePath(rootDir);
const subDirs = readAllFilePaths(rootDir, 0, 2, [], true);
// 获取所有子目录(与原代码一致)
const subDirs = readAllFilePaths(rootDir, 0, 0, [], true);
// 筛选目标目录(与原代码一致)
const targetDirs = isAll
? subDirs
: subDirs.filter(subDir => {
const dirName = basename(subDir);
return preSpecificNames.includes(dirName);
});
const targetDirs = subDirs.filter(subDir => {
const dirName = basename(subDir);
const hasIcon = hasIconFolder(subDir);
const matchName = isAll ? true : preSpecificNames.includes(dirName);
return hasIcon && matchName;
});
if (targetDirs.length === 0) {
log.info(`未找到与指定名称匹配的目录,名称列表:${preSpecificNames?.join(', ') || '所有'}`);
// log.info("未找到符合条件的弹窗目录");
return [];
}
// 预加载所有目录的资源原imageClick内的资源加载逻辑
const preloadedResources = [];
for (const subDir of targetDirs) {
const dirName = basename(subDir);
// log.info(`开始预处理弹窗类型:${dirName}`);
const fullPath = normalizePath(subDir);
const pathSegments = fullPath.slice(rootDirNormalized.length + 1).split('/');
const level = pathSegments.length;
const isFirstLevel = level === 1;
// 查找icon和Picture文件夹与原代码一致
const entries = readAllFilePaths(subDir, 0, 1, [], true);
const iconDir = entries.find(entry => entry.endsWith('\icon'));
const pictureDir = entries.find(entry => entry.endsWith('\Picture'));
let popupConfig = {
isSpecial: false,
operationType: "click",
ocrConfig: null,
xOffset: 0,
yOffset: 0,
detectRegion: null,
nextLevelOnSuccess: "",
nextLevelOnFailure: "",
loopCount: 1,
loopDelay: 0
};
const configPath = normalizePath(`${subDir}/config.json`);
let isSpecialModule = false;
let specialDetectRegion = null;
if (!iconDir) {
log.warn(`未找到 icon 文件夹,跳过分类文件夹:${subDir}`);
continue;
}
if (!pictureDir) {
log.warn(`未找到 Picture 文件夹,跳过分类文件夹:${subDir}`);
continue;
if (fileExists(configPath)) {
try {
const configContent = file.readTextSync(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
&& popupConfig.detectRegion.height > 0;
specialDetectRegion = isSpecialModule ? popupConfig.detectRegion : null;
// log.info(`【${dirName}】加载配置成功:${isFirstLevel ? '第一级' : '第二级'} | 模块类型:${isSpecialModule ? '特殊模块' : '普通模块'}`);
} catch (e) {
log.error(`${dirName}】解析配置失败,使用默认配置:${e.message}`);
isSpecialModule = false;
}
}
// 读取图片文件(与原代码一致)
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
// 预创建icon识别对象与原代码一致
const iconRecognitionObjects = [];
for (const filePath of iconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
if (isSpecialModule) {
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;
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080);
iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject, iconDir });
}
// 预创建图库区域(与原代码一致)
const pictureRegions = [];
for (const filePath of pictureFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图库图片失败:${filePath}`);
const iconRecognitionObjects = [];
for (const filePath of iconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`${dirName}】特殊模块加载图标失败:${filePath}`);
continue;
}
iconRecognitionObjects.push({
name: basename(filePath),
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
iconDir,
mat: mat
});
}
const targetIcon = iconRecognitionObjects[0];
const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y);
manualRegion.width = specialDetectRegion.width;
manualRegion.height = specialDetectRegion.height;
const foundRegions = [{
pictureName: "特殊模块",
iconName: targetIcon.name,
region: manualRegion,
iconDir: iconDir
}];
// log.info(`【${dirName}】特殊模块生成识别区域x=${manualRegion.x}, y=${manualRegion.y}, 宽=${manualRegion.width}, 高=${manualRegion.height}`);
preloadedResources.push({
dirName,
fullPath,
foundRegions,
popupConfig,
isFirstLevel: isFirstLevel,
level: level
});
} else {
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;
}
pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) });
}
// 预计算匹配区域(与原代码一致)
const foundRegions = [];
for (const picture of pictureRegions) {
for (const icon of iconRecognitionObjects) {
const foundRegion = picture.region.find(icon.ro);
if (foundRegion.isExist()) {
foundRegions.push({
pictureName: picture.name,
iconName: icon.name,
region: foundRegion,
iconDir: icon.iconDir
});
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文件跳过`);
continue;
}
if (pictureFilePaths.length === 0) {
log.warn(`${dirName}】普通模块无有效Picture文件跳过`);
continue;
}
const iconRecognitionObjects = [];
for (const filePath of iconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`${dirName}】加载图标失败:${filePath}`);
continue;
}
iconRecognitionObjects.push({
name: basename(filePath),
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
iconDir
});
}
const pictureRegions = [];
for (const filePath of pictureFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`${dirName}】加载图库图片失败:${filePath}`);
continue;
}
pictureRegions.push({
name: basename(filePath),
region: new ImageRegion(mat, 0, 0)
});
}
const foundRegions = [];
for (const picture of pictureRegions) {
for (const icon of iconRecognitionObjects) {
const foundRegion = picture.region.find(icon.ro);
if (foundRegion.isExist()) {
foundRegions.push({
pictureName: picture.name,
iconName: icon.name,
region: foundRegion,
iconDir: icon.iconDir
});
}
}
}
}
if (foundRegions.length === 0) {
log.warn(`${dirName}】普通模块无匹配图标,跳过`);
continue;
}
// 保存预处理结果
preloadedResources.push({
dirName,
foundRegions
});
preloadedResources.push({
dirName,
fullPath,
foundRegions,
popupConfig,
isFirstLevel: isFirstLevel,
level: level
});
}
}
log.info(`预加载完成,共处理 ${preloadedResources.length} 个目录`);
// log.info(`预加载完成,共${preloadedResources.length}个有效弹窗目录(第一级:${preloadedResources.filter(r => r.isFirstLevel).length}个,第二级:${preloadedResources.filter(r => !r.isFirstLevel).length}个)`);
return preloadedResources;
}
// 新增imageClick后台任务函数
// ======================== 2. 后台任务函数(精简日志版)========================
async function imageClickBackgroundTask() {
log.info("imageClick后台任务已启动");
const imageClickDelay = Math.min(99, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 5)))*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]+/)
.map(name => name.trim())
.filter(name => name !== "");
// 调用独立预加载函数(循环前仅执行一次)
// 预加载资源
const preloadedResources = await preloadImageResources(specificNames);
if (preloadedResources.length === 0) {
log.info("无可用预加载资源,任务结束");
log.info("无可用弹窗资源,任务结束");
return { success: false };
}
// 循环执行,仅使用预加载资源
// 筛选一级弹窗
const firstLevelDirs = preloadedResources.filter(res => res.isFirstLevel);
if (firstLevelDirs.length === 0) {
log.warn("无第一级弹窗目录,任务终止");
return { success: false };
}
// 打印资源检测结果
log.info("\n==================== 现有弹窗加载结果 ====================");
log.info("1. 一级弹窗(共" + firstLevelDirs.length + "个):");
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}`));
log.info("=============================================================\n");
// 核心逻辑:外循环遍历所有一级弹窗
while (!state.completed && !state.cancelRequested) {
try {
// 调用imageClick时传入预加载资源
await imageClick(preloadedResources, null, specificNames, true);
} catch (error) {
log.info(`弹窗识别失败(继续重试):${error.message}`);
// log.info(`\n===== 外循环开始:遍历所有一级弹窗(共${firstLevelDirs.length}个) =====`);
// 遍历所有一级弹窗
for (const currentFirstLevel of firstLevelDirs) {
// 检查当前一级弹窗是否被触发
const levelResult = await imageClick([currentFirstLevel], null, [currentFirstLevel.dirName], true);
if (levelResult.success) {
log.info(`${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
const levelStack = [currentFirstLevel];
// 内循环处理内部流程
while (levelStack.length > 0 && !state.completed && !state.cancelRequested) {
const currentResource = levelStack[levelStack.length - 1];
const innerResult = await imageClick([currentResource], null, [currentResource.dirName], true);
if (innerResult.success) {
const nextPath = normalizePath(currentResource.popupConfig.nextLevelOnSuccess);
if (nextPath && nextPath.trim()) {
const nextResource = preloadedResources.find(res => res.fullPath === nextPath);
if (nextResource) {
levelStack.push(nextResource);
} else {
// log.warn(`内循环:下一级(${nextPath})不存在,回退`);
levelStack.pop();
}
} else {
levelStack.pop();
}
} else {
const nextPath = normalizePath(currentResource.popupConfig.nextLevelOnFailure);
if (nextPath && nextPath.trim()) {
const nextResource = preloadedResources.find(res => res.fullPath === nextPath);
if (nextResource) {
levelStack.push(nextResource);
} else {
// log.warn(`内循环:下一级(${nextPath})不存在,回退`);
levelStack.pop();
}
} else {
levelStack.pop();
}
}
await sleep(100);
}
log.info(`${currentFirstLevel.dirName}】内部流程处理完毕`);
}
}
// 短暂等待后再次执行
await sleep(imageClickDelay);
// log.info(`===== 外循环结束:等待${taskDelay/1000}秒后开始下一次循环 =====`);
await sleep(taskDelay);
}
log.info("imageClick后台任务结束");
log.info("imageClick后台任务结束");
return { success: true };
}
// 优化:使用预加载资源,保留所有原执行逻辑
// ======================== 3. 识别与操作函数(精简日志版)========================
async function imageClick(preloadedResources, ra = null, specificNames = null, useNewScreenshot = false) {
// 保留原参数格式处理(兼容历史调用)
if (typeof specificNames === 'string') {
specificNames = [specificNames];
}
const isAll = !specificNames || specificNames.length === 0;
let isAnySuccess = false;
// 遍历预处理好的资源原targetDirs循环逻辑
for (const resource of preloadedResources) {
const { dirName, foundRegions } = resource;
const { dirName, foundRegions, popupConfig } = resource;
const { xOffset, yOffset } = popupConfig;
let hasAnyIconDetected = false; // 标记是否有图标被识别
for (const foundRegion of foundRegions) {
// 保留原识别对象创建逻辑(使用预处理的路径)
const tolerance = 1;
const iconMat = file.readImageMatSync(`${foundRegion.iconDir}/${foundRegion.iconName}`);
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,
foundRegion.region.x - tolerance,
foundRegion.region.y - tolerance,
foundRegion.region.width + 2 * tolerance,
foundRegion.region.height + 2 * tolerance
detectRegion?.x ?? defaultX,
detectRegion?.y ?? defaultY,
detectRegion?.width ?? defaultWidth,
detectRegion?.height ?? defaultHeight
);
recognitionObject.threshold = 0.85;
// 保留原识别逻辑
const result = await recognizeImage(
recognitionObject,
ra,
1000, // timeout
500, // interval
1000,
500,
useNewScreenshot,
dirName // iconType
dirName
);
// 保留原点击逻辑
if (result.isDetected && result.x !== 0 && result.y !== 0) {
const x = Math.round(result.x + result.width / 2);
const y = Math.round(result.y + result.height / 2);
log.info(`即将点击【${dirName}】类型下的图标:${foundRegion.iconName},位置: (${x}, ${y})`);
await click(x, y);
log.info(`点击【${dirName}】类型下的${foundRegion.iconName}成功`);
await sleep(10);
return { success: true };
} else {
// log.info(`未发现弹窗【${dirName}】的图标:${foundRegion.iconName}`);
hasAnyIconDetected = true;
isAnySuccess = true;
const centerX = Math.round(result.x + result.width / 2);
const centerY = Math.round(result.y + result.height / 2);
const actualX = centerX + xOffset;
const actualY = centerY + yOffset;
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); // 保留原始点击逻辑
if (i < clickCount - 1) await sleep(clickDelay); // 非最后一次加间隔
}
} else {
switch (popupConfig.operationType) {
case "key_press": {
const targetKey = popupConfig.keyCode || "VK_SPACE";
// 新增key_press用循环默认3次1000ms间隔与原硬编码逻辑一致
const pressCount = popupConfig.loopCount || 3;
const pressDelay = popupConfig.loopDelay || 1000;
for (let i = 0; i < pressCount; i++) {
keyPress(targetKey); // 保留原始按键逻辑
if (i < pressCount - 1) await sleep(pressDelay); // 非最后一次加间隔
}
log.info(`${dirName}】弹窗触发按键【${targetKey}】,共${pressCount}次,间隔${pressDelay}ms`);
break;
}
case "ocr_click": {
isAnySuccess = false;
const { targetTexts, xRange, yRange, timeout = 2000 } = popupConfig.ocrConfig || {};
if (!targetTexts || !xRange || !yRange) {
log.error(`${dirName}】弹窗OCR配置不全跳过`);
break;
}
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;
// 新增OCR点击加循环默认1次0间隔与原逻辑一致
const ocrCount = popupConfig.loopCount;
const ocrDelay = popupConfig.loopDelay;
for (let i = 0; i < ocrCount; i++) {
await click(ocrActualX, ocrActualY); // 保留原始OCR点击逻辑
if (i < ocrCount - 1) await sleep(ocrDelay); // 非最后一次加间隔
}
log.info(`${dirName}】弹窗OCR点击“${ocrResults[0].text}”:(${ocrActualX}, ${ocrActualY}),共${ocrCount}次,间隔${ocrDelay}ms`);
isAnySuccess = true;
} else {
log.warn(`${dirName}】弹窗OCR未识别到文本`);
}
break;
}
case "key_mouse_script": {
try {
const scriptPath = normalizePath(popupConfig.scriptPath || "");
if (!scriptPath) {
log.error(`${dirName}】弹窗未配置键鼠脚本路径,跳过执行`);
isAnySuccess = false;
break;
}
if (!fileExists(scriptPath)) {
log.error(`${dirName}】弹窗键鼠脚本不存在:${scriptPath}`);
isAnySuccess = false;
break;
}
// 新增键鼠脚本加循环默认1次0间隔与原逻辑一致
const scriptCount = popupConfig.loopCount;
const scriptDelay = popupConfig.loopDelay;
for (let i = 0; i < scriptCount; i++) {
await keyMouseScript.runFile(scriptPath); // 保留原始脚本执行逻辑
if (i < scriptCount - 1) await sleep(scriptDelay); // 非最后一次加间隔
}
log.info(`${dirName}】弹窗键鼠脚本执行完成,共${scriptCount}次,间隔${scriptDelay}ms`);
isAnySuccess = true;
} catch (error) {
log.error(`${dirName}】弹窗键鼠脚本执行失败:${error.message}`);
isAnySuccess = false;
}
break;
}
default:
log.error(`${dirName}】弹窗未知操作类型:${popupConfig.operationType},默认执行点击`);
// 新增默认操作加循环默认1次0间隔与原逻辑一致
const defaultCount = popupConfig.loopCount;
const defaultDelay = popupConfig.loopDelay;
for (let i = 0; i < defaultCount; i++) {
await click(actualX, actualY); // 保留原始默认点击逻辑
if (i < defaultCount - 1) await sleep(defaultDelay); // 非最后一次加间隔
}
isAnySuccess = true;
}
}
}
}
// 仅在该弹窗无任何图标识别时输出警告
if (!hasAnyIconDetected && !isAnySuccess) {
// log.warn(`【${dirName}】弹窗未发现任何有效图标`);
}
await sleep(10);
}
// 所有目标处理完毕仍未成功(保留原返回逻辑)
return { success: false };
return { success: isAnySuccess, message: isAnySuccess ? "弹窗识别操作成功" : "未识别到弹窗" };
}

View File

@@ -111,7 +111,6 @@ function parseMonsterMaterials() {
}
materialToMonsters[mat].add(monsterName);
});
log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}解析怪物材料:${monsterName} -> [${materials.join(', ')}]`);
}
});
} catch (error) {
@@ -155,7 +154,11 @@ function getSelectedMaterialCategories() {
return acc;
}, {});
const finalSettings = { ...initialSettings, ...settings };
const finalSettings = Object.keys(initialSettings).reduce((acc, key) => {
// 若settings中有该键则使用其值否则用默认的false确保只处理material_mapping中的键
acc[key] = settings.hasOwnProperty(key) ? settings[key] : initialSettings[key];
return acc;
}, {});
return Object.keys(finalSettings)
.filter(key => key !== "unselected")
@@ -294,6 +297,7 @@ function readMaterialCD() {
const materialCDCategories = {};
for (const filePath of materialFilePaths) {
if (state.cancelRequested) break;
const content = file.readTextSync(filePath);
if (!content) {
log.error(`${CONSTANTS.LOG_MODULES.CD}加载文件失败:${filePath}`);
@@ -1156,7 +1160,6 @@ function classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMateri
if (pathEntries.length > 0) {
log.info(`${CONSTANTS.LOG_MODULES.PATH}\n===== 匹配到的材料路径列表 =====`);
pathEntries.forEach((entry, index) => {
log.info(`${index + 1}. 材料:${entry.resourceName},路径:${entry.path}`);
});
log.info(`=================================\n`);
} else {
@@ -1355,6 +1358,11 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
=========================================\n\n`;
writeContentToFile(summaryPath, content);
log.info(`${CONSTANTS.LOG_MODULES.RECORD}最终汇总已记录至 ${summaryPath}`);
// ==============================================
// 新增:汇总后强制触发结束指令,确保程序终止
// ==============================================
state.completed = true; // 标记任务完全完成
state.cancelRequested = true; // 终止所有后台任务如图像点击、OCR
}
// ==============================================
@@ -1387,11 +1395,15 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
}
// 关键补充等待超量名单生成由filterLowCountMaterials更新
let waitTimes = 0;
while (excessMaterialNames.length === 0 && waitTimes < 300) {
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
await sleep(1000); // 每1秒查一次
waitTimes++;
}
// 若收到终止信号直接退出OCR任务不再执行后续逻辑
if (state.cancelRequested) {
log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号已退出`);
return;
}
// 现在过滤才有效确保excessMaterialNames已生成
allTargetTexts = allTargetTexts.filter(name => !excessMaterialNames.includes(name));
log.info(`OCR最终目标文本已过滤超量${allTargetTexts.join('、')}`);

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "背包统计采集系统",
"version": "2.52",
"version": "2.53",
"bgi_version": "0.44.8",
"description": "可统计背包养成道具、部分食物、素材的数量根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势\n+ 1. 自动判断材料CD不需要管材料CD有没有好\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别不会交互路边的npc或是神像可自定义识别名单具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料自动屏蔽更多详细内容查看readme.md",
"saved_files": [

View File

@@ -102,12 +102,27 @@
{
"name": "PopupClickDelay",
"type": "input-text",
"label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔默认:5 秒)"
"label": "如 过期物品,信件自定义文件夹名。注意文件夹结构\n----------------------------------\n弹窗循环间隔默认:15 秒)"
},
{
"name": "CDCategories",
"type": "input-text",
"label": "----------------------------------\n\n采用的CD分类默认:全部)"
"label": "====================\n\n采用的CD分类默认全部) 举例:采集,怪物,木材"
},
{
"name": "PickCategories",
"type": "input-text",
"label": "根据CD分类来加载路径文件具体看materialsCD目录\n====================\n\n采用的识别名单默认全部) 举例:交互,采集,宝箱"
},
{
"name": "ExceedCount",
"type": "input-text",
"label": "根据拾取分类来加载OCR名单具体看targetText目录\n----------------------------------\n\n超量阈值默认5000)超量的路径材料将不拾取"
},
{
"name": "PageScrollDistance",
"type": "input-text",
"label": "====================\n拖动距离(默认711像素点)"
},
{
"name": "HoldX",
@@ -118,10 +133,5 @@
"name": "HoldY",
"type": "input-text",
"label": "------------------------\n翻页拖动点Y坐标0~1080(默认750)"
},
{
"name": "PageScrollDistance",
"type": "input-text",
"label": "------------------------\n拖动距离(默认711像素点)"
}
]
]