diff --git a/repo/js/背包材料统计/README.md b/repo/js/背包材料统计/README.md index 84fc2575d..a722bc2f3 100644 --- a/repo/js/背包材料统计/README.md +++ b/repo/js/背包材料统计/README.md @@ -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收获记录,三次及以上同名路径记录,就会触发排除); 操作参考截图:
@@ -224,4 +224,10 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
| v2.50 | 新增独立名单拾取、弹窗模块;支持怪物名识别 |
| v2.51 | 自定义设置新增“拖动距离/拖动点”;新增月卡弹窗识别;路径材料超量自动上黑名单;修复怪物0收获记录 |
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |
-| v2.54 | 自定义设置新增“终止时刻”,修复bug,新增“添加不规范命名的路径文件夹”说明,新增一个“锄地”的怪物路径CD |
\ No newline at end of file
+| 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报错 |
\ No newline at end of file
diff --git a/repo/js/背包材料统计/lib/autoPick.js b/repo/js/背包材料统计/lib/autoPick.js
index f7aa33f71..ff1890a4d 100644
--- a/repo/js/背包材料统计/lib/autoPick.js
+++ b/repo/js/背包材料统计/lib/autoPick.js
@@ -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;
}
diff --git a/repo/js/背包材料统计/lib/backStats.js b/repo/js/背包材料统计/lib/backStats.js
index 87e20d1ee..383c029b7 100644
--- a/repo/js/背包材料统计/lib/backStats.js
+++ b/repo/js/背包材料统计/lib/backStats.js
@@ -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;
diff --git a/repo/js/背包材料统计/lib/file.js b/repo/js/背包材料统计/lib/file.js
index b960e84a1..f566018ff 100644
--- a/repo/js/背包材料统计/lib/file.js
+++ b/repo/js/背包材料统计/lib/file.js
@@ -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;
}
diff --git a/repo/js/背包材料统计/lib/imageClick.js b/repo/js/背包材料统计/lib/imageClick.js
index 430de9df5..8e514eec1 100644
--- a/repo/js/背包材料统计/lib/imageClick.js
+++ b/repo/js/背包材料统计/lib/imageClick.js
@@ -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;
diff --git a/repo/js/背包材料统计/lib/ocr.js b/repo/js/背包材料统计/lib/ocr.js
index 44aef5aae..d8f0db10e 100644
--- a/repo/js/背包材料统计/lib/ocr.js
+++ b/repo/js/背包材料统计/lib/ocr.js
@@ -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 // 超时前最后一次有效截图(可用于排查原因)
+ };
+}
diff --git a/repo/js/背包材料统计/lib/region.js b/repo/js/背包材料统计/lib/region.js
index dd5461684..422d7b4a2 100644
--- a/repo/js/背包材料统计/lib/region.js
+++ b/repo/js/背包材料统计/lib/region.js
@@ -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;
+}
\ No newline at end of file
diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js
index 53721c5a6..b141cf57b 100644
--- a/repo/js/背包材料统计/main.js
+++ b/repo/js/背包材料统计/main.js
@@ -1,3 +1,74 @@
+// ==============================================
+// 内容检测码生成(通用哈希逻辑)
+// ==============================================
+function generateContentCode(positions) {
+ try {
+ const serialized = JSON.stringify(
+ positions.map(pos => ({
+ type: pos.type,
+ x: parseFloat(pos.x).toFixed(2),
+ y: parseFloat(pos.y).toFixed(2)
+ }))
+ );
+ let hash = 0;
+ for (let i = 0; i < serialized.length; i++) {
+ const char = serialized.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // 转换为32位整数
+ }
+ return ((hash >>> 0).toString(16).padStart(8, '0')).slice(-8);
+ } catch (error) {
+ log.warn(`生成检测码失败: ${error.message},使用默认值`);
+ return "00000000";
+ }
+}
+
+/**
+ * 从文件名中提取内容检测码
+ * @param {string} fileName - 文件名
+ * @returns {string|null} 内容检测码,未找到返回null
+ */
+function extractContentCodeFromFileName(fileName) {
+ const match = fileName.match(/_([0-9a-fA-F]{8})\.json$/);
+ return match ? match[1].toLowerCase() : null;
+}
+
+/**
+ * 读取路径文件并生成内容检测码
+ * @param {string} pathingFilePath - 路径文件路径
+ * @returns {string} 内容检测码
+ */
+function generatePathContentCode(pathingFilePath) {
+ try {
+ // 从文件名中提取检测码
+ const fileName = basename(pathingFilePath);
+ const extractedCode = extractContentCodeFromFileName(fileName);
+ if (extractedCode) {
+ return extractedCode;
+ }
+
+ // 如果文件名中没有检测码,生成新的
+ const content = safeReadTextSync(pathingFilePath);
+ if (!content) {
+ log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件为空: ${pathingFilePath}`);
+ return "00000000";
+ }
+
+ const pathData = JSON.parse(content);
+ const positions = pathData.positions || pathData.actions || [];
+
+ if (!Array.isArray(positions) || positions.length === 0) {
+ log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件无有效位置数据: ${pathingFilePath}`);
+ return "00000000";
+ }
+
+ return generateContentCode(positions);
+ } catch (error) {
+ log.warn(`${CONSTANTS.LOG_MODULES.PATH}生成路径检测码失败: ${error.message}`);
+ return "00000000";
+ }
+}
+
// ==============================================
// 常量与配置(集中管理硬编码值)
// ==============================================
@@ -10,14 +81,15 @@ const CONSTANTS = {
NO_RECORD_DIR: "pathing_record/noRecord",
IMAGES_DIR: "assets/images",
MONSTER_MATERIALS_PATH: "assets/Monster-Materials.txt",
-
+
// 解析与处理配置
MAX_PATH_DEPTH: 3, // 路径解析最大深度
NOTIFICATION_CHUNK_SIZE: 500, // 通知拆分长度
FOOD_EXP_RECORD_SUFFIX: "_狗粮.txt",
+ FOOD_ZERO_EXP_SUFFIX: "_狗粮-0.txt", // 新增:狗粮0 EXP记录后缀
SUMMARY_FILE_NAME: "材料收集汇总.txt",
ZERO_COUNT_SUFFIX: "-0.txt",
-
+
// 日志模块标识
LOG_MODULES: {
INIT: "[初始化]",
@@ -34,12 +106,12 @@ const CONSTANTS = {
// 引入外部脚本(源码不变)
// ==============================================
eval(file.readTextSync("lib/file.js"));
-eval(file.readTextSync("lib/ocr.js"));
-eval(file.readTextSync("lib/autoPick.js"));
-eval(file.readTextSync("lib/exp.js"));
-eval(file.readTextSync("lib/backStats.js"));
-eval(file.readTextSync("lib/imageClick.js"));
-eval(file.readTextSync("lib/displacement.js"));
+eval(safeReadTextSync("lib/ocr.js"));
+eval(safeReadTextSync("lib/autoPick.js"));
+eval(safeReadTextSync("lib/exp.js"));
+eval(safeReadTextSync("lib/backStats.js"));
+eval(safeReadTextSync("lib/imageClick.js"));
+eval(safeReadTextSync("lib/displacement.js"));
// ==============================================
// 全局状态(保持不变)
@@ -53,8 +125,8 @@ const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost)
const notify = settings.notify || false;
const noRecord = settings.noRecord || false;
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 endTimeStr = settings.CurrentTime ? settings.CurrentTime : null;
+const exceedCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.ExceedCount) || 9000))); // 设定的超量目标数量
+const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null;
// 解析需要处理的CD分类
const allowedCDCategories = (settings.CDCategories || "")
@@ -71,20 +143,21 @@ if (allowedCDCategories.length > 0) {
// ==============================================
// 材料与怪物映射管理
// ==============================================
-// 材料分类映射
+// 材料分类映射 - 适配新的settings.json结构(只保留新的中文分类名称映射)
const material_mapping = {
- "General": "一般素材",
- "Drops": "怪物掉落素材",
- "CookingIngs": "烹饪食材",
- "ForagedFood": "采集食物",
- "Weekly": "周本素材",
- "Wood": "木材",
- "CharAscension": "角色突破素材",
- "Fishing": "鱼饵鱼类",
- "Smithing": "锻造素材",
- "Gems": "宝石",
- "Talent": "角色天赋素材",
- "WeaponAscension": "武器突破素材",
+ // 适配新的settings.json分类选项
+ "矿石、原胚": "锻造素材",
+ "经验书、怪物掉落": "怪物掉落素材",
+ "一般素材": "一般素材",
+ "采集食物": "采集食物",
+ "烹饪用食材": "烹饪食材",
+ "世界BOSS": "角色突破素材",
+ "木材": "木材",
+ "鱼饵、鱼类": "鱼饵鱼类",
+ "宝石": "宝石",
+ "天赋素材": "角色天赋素材",
+ "武器突破": "武器突破素材",
+ "祝圣精华": "祝圣精华"
};
// 怪物-材料映射(双向,优化为Set提高查找效率)
@@ -97,16 +170,16 @@ let materialToMonsters = {}; // 材料名 -> Set(关联怪物列表)
*/
function parseMonsterMaterials() {
try {
- const content = file.readTextSync(CONSTANTS.MONSTER_MATERIALS_PATH);
+ const content = safeReadTextSync(CONSTANTS.MONSTER_MATERIALS_PATH);
const lines = content.split('\n').map(line => line.trim()).filter(line => line);
-
+
lines.forEach(line => {
if (!line.includes(':')) return;
const [monsterName, materialsStr] = line.split(':');
const materials = materialsStr.split(/[,,、 \s]+/)
.map(mat => mat.trim())
.filter(mat => mat);
-
+
if (monsterName && materials.length > 0) {
monsterToMaterials[monsterName] = materials;
materials.forEach(mat => {
@@ -127,7 +200,8 @@ parseMonsterMaterials(); // 初始化怪物材料映射
// 路径模式配置
// ==============================================
const pathingValue = settings.Pathing || '';
-const pathingPrefix = String(pathingValue).split('.')[0];
+// 从选择字符串中提取前缀数字,适配新的settings.json结构
+const pathingPrefix = pathingValue.match(/^(\d+)/) ? pathingValue.match(/^(\d+)/)[1] : '';
const pathingMode = {
includeBoth: pathingPrefix === "1",
@@ -148,38 +222,54 @@ if (pathingMode.onlyPathing) log.warn(`${CONSTANTS.LOG_MODULES.PATH}已开启【
// ==============================================
// 材料分类处理
// ==============================================
-/**
- * 初始化并筛选选中的材料分类
- * @returns {string[]} 选中的材料分类列表
- */
+/**
+ * 初始化并筛选选中的材料分类(适配multi-checkbox的Categories配置)
+ * @returns {string[]} 选中的材料分类列表
+ */
function getSelectedMaterialCategories() {
- const initialSettings = Object.keys(material_mapping).reduce((acc, key) => {
- acc[key] = false;
- return acc;
- }, {});
+ // 使用Array.from()确保将settings.Categories转换为真正的数组,适配multi-checkbox返回的类数组对象
+ let selectedCategories = [];
- const finalSettings = Object.keys(initialSettings).reduce((acc, key) => {
- // 若settings中有该键则使用其值,否则用默认的false(确保只处理material_mapping中的键)
- acc[key] = settings.hasOwnProperty(key) ? settings[key] : initialSettings[key];
- return acc;
- }, {});
+ try {
+ selectedCategories = Array.from(settings.Categories || []);
+ } catch (e) {
+ log.error(`${CONSTANTS.LOG_MODULES.MATERIAL}获取分类设置失败: ${e.message}`);
+ }
- return Object.keys(finalSettings)
- .filter(key => key !== "unselected")
- .filter(key => {
- if (typeof finalSettings[key] !== 'boolean') {
- log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}非布尔值的键: ${key}, 值: ${finalSettings[key]}`);
- return false;
+ // 兼容旧的checkbox字段名
+ if (!selectedCategories || selectedCategories.length === 0) {
+ const checkboxToCategory = {
+ "Smithing": "矿石、原胚",
+ "Drops": "经验书、怪物掉落",
+ "ForagedFood": "采集食物",
+ "General": "一般素材",
+ "CookingIngs": "烹饪用食材",
+ "Weekly": "周本素材",
+ "Wood": "木材",
+ "CharAscension": "世界BOSS",
+ "Fishing": "鱼饵、鱼类",
+ "Gems": "宝石",
+ "Talent": "天赋素材",
+ "WeaponAscension": "武器突破",
+ "XP": "祝圣精华"
+ };
+
+ Object.keys(checkboxToCategory).forEach(checkboxName => {
+ if (settings[checkboxName] === true) {
+ selectedCategories.push(checkboxToCategory[checkboxName]);
}
- return finalSettings[key];
- })
- .map(name => {
- if (!material_mapping[name]) {
- log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}material_mapping中缺失的键: ${name}`);
- return null;
- }
- return material_mapping[name];
- })
+ });
+ }
+
+ // 默认分类
+ if (!selectedCategories || selectedCategories.length === 0) {
+ selectedCategories = ["一般素材", "烹饪用食材"];
+ }
+
+ // 过滤无效值并映射到实际分类
+ return selectedCategories
+ .filter(cat => typeof cat === 'string' && cat !== "")
+ .map(name => material_mapping[name] || "锻造素材")
.filter(name => name !== null);
}
@@ -302,7 +392,7 @@ function readMaterialCD() {
for (const filePath of materialFilePaths) {
if (state.cancelRequested) break;
- const content = file.readTextSync(filePath);
+ const content = safeReadTextSync(filePath);
if (!content) {
log.error(`${CONSTANTS.LOG_MODULES.CD}加载文件失败:${filePath}`);
continue;
@@ -319,7 +409,7 @@ function readMaterialCD() {
}
// ==============================================
-// 时间工具
+// 时间工具(核心修改:新增剩余时间计算)
// ==============================================
/**
* 获取当前时间(小时,含小数)
@@ -357,7 +447,7 @@ function getRemainingMinutesToEndTime(endTimeStr) {
}
// ==============================================
-// 记录管理
+// 记录管理(核心修改:公共函数+缓存复用)
// ==============================================
/**
* 写入内容到文件(追加模式)
@@ -368,7 +458,7 @@ function writeContentToFile(filePath, content) {
try {
let existingContent = '';
try {
- existingContent = file.readTextSync(filePath);
+ existingContent = safeReadTextSync(filePath);
} catch (readError) {
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}文件不存在或读取失败: ${filePath}`);
}
@@ -390,14 +480,20 @@ function writeContentToFile(filePath, content) {
* @param {string} resourceName - 资源名
* @param {string} pathName - 路径名
* @param {string} recordDir - 记录目录
+ * @param {boolean} isFood - 是否为狗粮路径(新增参数)
* @returns {boolean} 是否允许运行(true=允许)
*/
-function checkPathNameFrequency(resourceName, pathName, recordDir) {
- const recordPath = `${recordDir}/${resourceName}${CONSTANTS.ZERO_COUNT_SUFFIX}`;
+function checkPathNameFrequency(resourceName, pathName, recordDir, isFood = false) {
+ // ========== 核心修改:适配狗粮0记录文件 ==========
+ let suffix = CONSTANTS.ZERO_COUNT_SUFFIX; // 普通材料默认-0.txt
+ if (isFood) {
+ suffix = CONSTANTS.FOOD_ZERO_EXP_SUFFIX; // 狗粮用_狗粮-0.txt
+ }
+ const recordPath = `${recordDir}/${resourceName}${suffix}`;
let totalCount = 0;
try {
- const content = file.readTextSync(recordPath);
+ const content = safeReadTextSync(recordPath);
const lines = content.split('\n');
lines.forEach(line => {
@@ -406,11 +502,13 @@ function checkPathNameFrequency(resourceName, pathName, recordDir) {
}
});
} catch (error) {
- log.debug(`${CONSTANTS.LOG_MODULES.RECORD}目录${recordDir}中无${resourceName}记录,跳过检查`);
+ log.debug(`${CONSTANTS.LOG_MODULES.RECORD}目录${recordDir}中无${resourceName}${suffix}记录,跳过检查`);
}
+ // 重复次数≥3则禁止运行(仅统计0记录)
if (totalCount >= 3) {
- log.info(`${CONSTANTS.LOG_MODULES.RECORD}路径文件: ${pathName},普通模式累计0采集${totalCount}次,请清理记录后再执行`);
+ const typeDesc = isFood ? "狗粮" : "普通材料";
+ log.info(`${CONSTANTS.LOG_MODULES.RECORD}${typeDesc}路径文件: ${pathName},累计0 EXP/0数量运行${totalCount}次,请清理记录后再执行`);
return false;
}
return true;
@@ -427,12 +525,15 @@ function checkPathNameFrequency(resourceName, pathName, recordDir) {
* @param {Object} materialCountDifferences - 材料数量变化
* @param {number} finalCumulativeDistance - 累计移动距离
*/
-function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}, finalCumulativeDistance) {
+function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}, finalCumulativeDistance, pathingFilePath) {
const recordPath = `${recordDir}/${resourceName}.txt`;
- const normalContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
+ // 生成内容检测码
+ const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000";
+ const normalContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
try {
if (runTime >= 3) { // 运行时间≥3秒才处理记录
+ // 怪物路径专用逻辑(判断对应材料总数量是否为0)
const isMonsterPath = monsterToMaterials.hasOwnProperty(resourceName); // 是否为怪物路径
if (isMonsterPath) {
// 1. 获取当前怪物对应的所有目标材料(从已有映射中取)
@@ -445,21 +546,23 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
// 3. 若总数量为0,生成怪物专用0记录文件(文件名含“总0”标识,避免混淆)
if (monsterMaterialsTotal === 0) {
const zeroMonsterPath = `${recordDir}/${resourceName}${CONSTANTS.ZERO_COUNT_SUFFIX}`;
- const zeroMonsterContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
+ const zeroMonsterContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
writeContentToFile(zeroMonsterPath, zeroMonsterContent);
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}怪物【${resourceName}】对应材料总数量为0,已写入单独文件: ${zeroMonsterPath}`);
}
}
+ // 普通材料0记录逻辑
for (const [material, count] of Object.entries(materialCountDifferences)) {
if (material === resourceName && count === 0) {
const zeroMaterialPath = `${recordDir}/${material}${CONSTANTS.ZERO_COUNT_SUFFIX}`;
- const zeroMaterialContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
+ const zeroMaterialContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
writeContentToFile(zeroMaterialPath, zeroMaterialContent);
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}材料数目为0,已写入单独文件: ${zeroMaterialPath}`);
}
}
+ // 正常记录生成逻辑
const hasZeroMaterial = Object.values(materialCountDifferences).includes(0);
const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0;
@@ -487,29 +590,56 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
* @param {string} noRecordDir - 无记录目录
* @returns {string|null} 上次结束时间字符串(null=无记录)
*/
-function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir) {
+function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath) {
const checkDirs = [recordDir, noRecordDir];
let latestEndTime = null;
+ // 生成内容检测码
+ const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : null;
+
+ // 清理路径名中的检测码
+ const cleanPathName = pathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json');
+
checkDirs.forEach(dir => {
const recordPath = `${dir}/${resourceName}.txt`;
try {
- const content = file.readTextSync(recordPath);
+ const content = safeReadTextSync(recordPath);
const lines = content.split('\n');
- for (let i = 0; i < lines.length; i++) {
- if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) {
- const endTimeLine = lines[i + 2];
- if (endTimeLine?.startsWith('结束时间: ')) {
- const endTimeStr = endTimeLine.split('结束时间: ')[1];
- const endTime = new Date(endTimeStr);
+ // 按空行分割成记录块
+ const recordBlocks = content.split('\n\n').filter(block => block.includes('路径名: '));
- if (!latestEndTime || endTime > new Date(latestEndTime)) {
- latestEndTime = endTimeStr;
- }
+ recordBlocks.forEach(block => {
+ const blockLines = block.split('\n');
+ let blockPathName = '';
+ let blockContentCode = '00000000';
+ let blockEndTime = null;
+
+ blockLines.forEach(line => {
+ if (line.startsWith('路径名: ')) {
+ blockPathName = line.split('路径名: ')[1];
+ } else if (line.startsWith('内容检测码: ')) {
+ blockContentCode = line.split('内容检测码: ')[1] || '00000000';
+ } else if (line.startsWith('结束时间: ')) {
+ blockEndTime = line.split('结束时间: ')[1];
+ }
+ });
+
+ // 清理记录中的路径名检测码
+ const cleanBlockPathName = blockPathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json');
+
+ // 匹配条件:路径名相同 或者 内容检测码相同(新逻辑)
+ const isPathMatch = cleanBlockPathName === cleanPathName;
+ const isContentCodeMatch = contentCode && blockContentCode === contentCode;
+ const isMatch = isPathMatch || isContentCodeMatch;
+
+ if (isMatch && blockEndTime) {
+ const endTime = new Date(blockEndTime);
+ if (!latestEndTime || endTime > new Date(latestEndTime)) {
+ latestEndTime = blockEndTime;
}
}
- }
+ });
} catch (error) {
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}目录${dir}中无${resourceName}记录,跳过检查`);
}
@@ -519,17 +649,26 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir) {
}
/**
- * 计算单次时间成本(平均耗时/材料数量)
- * @param {string} resourceName - 资源名(普通材料名/怪物名)
+ * 公共函数:读取路径历史记录(支持缓存复用,避免重复读文件)
+ * @param {string} resourceKey - 记录键(怪物名/材料名)
* @param {string} pathName - 路径名
- * @param {string} recordDir - 记录目录
- * @returns {number|null} 时间成本(秒/中级单位),null=无法计算
+ * @param {string} recordDir - 主记录目录
+ * @param {string} noRecordDir - 备用记录目录
+ * @param {boolean} isFood - 是否为狗粮路径
+ * @param {Object} cache - 缓存对象(单次路径处理周期内有效)
+ * @returns {Array