diff --git a/repo/js/背包材料统计/README.md b/repo/js/背包材料统计/README.md index a722bc2f3..defdb80de 100644 --- a/repo/js/背包材料统计/README.md +++ b/repo/js/背包材料统计/README.md @@ -1,4 +1,4 @@ -# 背包材料统计 v2.60 +# 背包材料统计 v2.61 作者:吉吉喵 @@ -41,6 +41,8 @@ 3. **独立名单识别**:不与路边NPC、神像交互;可自定义识别名单(操作见「四、问题解答Q4」); 4. **实时弹窗保护**:内置弹窗模块(覆盖路边信件、过期物品、月卡、调查等场景),运行时全程保护路径不被弹窗干扰。 5. **自动黑名单**:内置拾取模块,联动材料统计,可识别爆满的路径材料,自动屏蔽。 +6. **路径检测码**:根据路径生成检测码,可识别改名单坐标未变的路径文件,自动匹配带检测码的路径记录。 +7. **怪物材料防爆仓**:对怪物材料路径添加多重机制,防止蓝紫怪物材料爆仓(怪物材料路径执行时,不会拾其他的怪物材料,除非pathing文件夹里存在且未超量)。 ## 二、用前须知 @@ -116,11 +118,11 @@ | 4. 发送通知 | ① 每类材料跑完通知一次;② 全部材料跑完汇总通知一次(需开启BGI通知) | 建议开启,方便实时了解进度(接收端如企业微信需自行配置) | | 5. 取消扫描 | 取消“每个路径执行后”的背包扫描,仅保留“全部执行前/后”2次扫描 | 有效路径记录达3条以上时可以开启,可节约运行时间 | | 6. 仅 pathing 材料 | 仅扫描 `pathing` 文件夹内的材料,跳过其他分类,大幅缩短扫描时间 | 路径配置完成后开启,提升脚本运行效率 | -| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) | -| 8. 采用的 CD 分类 | 不填则默认执行 `materialsCD` 文件夹内配置的CD分类;填写则仅执行指定CD分类 | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答Q2」) | +| 7. 弹窗名 | 不选则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) | +| 8. 采用的 CD 分类 | 不选则默认执行 `materialsCD` 文件夹内配置的CD分类;填写则仅执行指定CD分类 | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答Q2」) | | 9. 终止时刻 | 不填则不执行定时终止;路径无时间记录时,会预判路径耗时5分钟,且预留2分钟空闲 | 填写需要按24小时格式(例:4:10) | -| 10. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时,需符合配置规则(操作见「四、问题解答Q4」) | -| 11. 超量阈值 | 首次扫描后,超量的路径材料,将从识别名单中剔除,默认5000 | 不推荐9999,怪物材料有几千就够了,采用默认数值,可自动避免爆背包 | +| 10. 采用的识别名单 | 不选则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时,需符合配置规则(操作见「四、问题解答Q4」) | +| 11. 超量阈值 | 首次扫描后,超量的路径材料,将从识别名单中剔除,默认9000 | 不推荐9999,怪物材料有几千就够了,采用默认数值,可自动避免爆背包 | | 12. 拖动距离 | 解决非1080p分辨率下“划页过头”问题,需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”;大于1080p屏可适当减小数值 | @@ -130,7 +132,7 @@ 3. **食物识别强制要求**:背包食物界面**第一行必须包含8种食物**(苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪),缺少则这些食物无法识别; 4. **关键文件备份**:建议不定期备份 `pathing` 文件夹(路径文件)和 `pathing_record` 文件夹(路径运行记录),便于丢失后或被污染后,记录能恢复如初; 5. **OCR配置**:默认最新,调整识别名单时,用的是V5Auto; -6. **手动终止运行**:如果要终止JS运行,推荐在当前路径采集到当前材料前,或者采集完进入背包扫描时终止(会在扫描结束后终止),以保护当前记录;如果是【取消扫描】模式,不会储存当前记录的材料数目,就随意。 +6. **手动终止运行**:手动终止路径会被记录成noRecord模式,只参与CD计算,如果是【取消扫描】模式,不会储存当前记录的材料数目,两种情况都随意。 ## 五、问题解答 @@ -230,4 +232,5 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各 | v2.57 | 补全圣遗物无CD管理bug | | v2.58 | 优化背包扫图逻辑 | | v2.59 | 修复自动拾取匹配bug,改为双向匹配 | -| v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错 | \ No newline at end of file +| v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;材料分类升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错;检测码识别路径 | +| v2.61 | 背包材料识别机制加速;修复手动终止路径noRecord模式的额外条件判断不生效;全局图片缓存;识别名单、CD文件和弹窗升级多选框UI!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! | \ No newline at end of file diff --git a/repo/js/背包材料统计/lib/autoPick.js b/repo/js/背包材料统计/lib/autoPick.js index ff1890a4d..dc90dafd3 100644 --- a/repo/js/背包材料统计/lib/autoPick.js +++ b/repo/js/背包材料统计/lib/autoPick.js @@ -26,14 +26,34 @@ function parseCategoryContent(content) { // 从 targetText 文件夹中读取分类信息(无修改,与OCR无关) function readtargetTextCategories(targetTextDir) { - const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1); + const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1, ['.txt']); const materialCategories = {}; - // 解析筛选名单 - const pickTextNames = (settings.PickCategories || "") - .split(/[,,、 \s]+/).map(n => n.trim()).filter(n => n); + let pickTextNames = []; + try { + pickTextNames = Array.from(settings.PickCategories || []); + } catch (e) { + log.error(`获取PickCategories设置失败: ${e.message}`); + } + + let availablePickCategories = []; + try { + availablePickCategories = targetTextFilePaths.map(filePath => basename(filePath).replace('.txt', '')); + log.info(`可用识别名单:${availablePickCategories.join(', ')}`); + } catch (e) { + log.error(`扫描识别名单目录失败: ${e.message}`); + } + + if (pickTextNames.length === 0) { + log.info("未指定识别名单,将加载所有文件"); + } else { + const invalidCategories = pickTextNames.filter(name => !availablePickCategories.includes(name)); + if (invalidCategories.length > 0) { + log.warn(`以下识别名单不存在,将被忽略:${invalidCategories.join(', ')}`); + pickTextNames = pickTextNames.filter(name => availablePickCategories.includes(name)); + } + } - // 兜底日志:确认pickTextNames是否为空,方便排查 log.info(`筛选名单状态:${pickTextNames.length === 0 ? '未指定(空),将加载所有文件' : '指定了:' + pickTextNames.join(',')}`); for (const filePath of targetTextFilePaths) { @@ -92,36 +112,38 @@ const ScrollRo = RecognitionObject.TemplateMatch( /** * 对齐并交互目标(核心改造:适配最新版performOcr) - * @param {string[]} targetTexts - 待匹配的目标文本列表 + * @param {string[]|Function} targetTextsOrFunc - 待匹配的目标文本列表或函数 * @param {Object} fDialogueRo - F图标的识别对象 * @param {Object} textxRange - 文本识别的X轴范围 { min: number, max: number } * @param {number} texttolerance - 文本与F图标Y轴对齐的容差 * @param {Object} cachedFrame - 缓存的图像帧(可选) */ -async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame = null) { +async function alignAndInteractTarget(targetTextsOrFunc, fDialogueRo, textxRange, texttolerance, cachedFrame = null) { let lastLogTime = Date.now(); - const recognitionCount = new Map(); // 避免误触:文本+Y坐标 → 计数 - const ocrScreenshots = []; // 收集最新版performOcr返回的截图,统一释放 + const recognitionCount = new Map(); + const ocrScreenshots = []; try { while (!state.completed && !state.cancelRequested) { - recognitionCount.clear(); // 每次循环开始时清空计数 + if (state.ocrPaused) { + await sleep(100); + continue; + } + recognitionCount.clear(); const currentTime = Date.now(); - // 每10秒输出检测日志(保留原逻辑) + if (currentTime - lastLogTime >= 10000) { log.info("独立OCR识别中..."); lastLogTime = currentTime; } await sleep(50); - // 1. 释放上一帧缓存,捕获新帧(保留原逻辑) if (cachedFrame) { if (cachedFrame.Dispose) cachedFrame.Dispose(); else if (cachedFrame.dispose) cachedFrame.dispose(); } cachedFrame = captureGameRegion(); - // 2. 识别F图标/Scroll.png(保留原逻辑) let fRes = await findFIcon(fDialogueRo, 10, cachedFrame); if (!fRes) { const scrollRes = await findFIcon(ScrollRo, 10, cachedFrame); @@ -132,21 +154,19 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text continue; } - // 3. 核心改造:调用最新版performOcr - // 适配点1:参数顺序调整为「targetTexts, xRange, yRange, ra, timeout, interval」 - // 适配点2:接收返回的「results+screenshot」,并收集screenshot - const yRange = { min: fRes.y - 3, max: fRes.y + 37 }; // 原Y轴范围不变 - const { results: ocrResults, screenshot: ocrScreenshot } = await performOcr( - targetTexts, // 目标文本列表(原逻辑) - textxRange, // 文本X轴范围(原逻辑) - yRange, // 文本Y轴范围(原逻辑) - cachedFrame, // 初始截图(最新版:第4个参数为ra) - 10, // 超时时间(保留原10ms) - 5 // 重试间隔(保留原5ms) - ); - ocrScreenshots.push(ocrScreenshot); // 收集截图,避免内存泄漏 + const targetTexts = typeof targetTextsOrFunc === 'function' ? targetTextsOrFunc() : targetTextsOrFunc; + + const yRange = { min: fRes.y - 3, max: fRes.y + 37 }; + const { results: ocrResults, screenshot: ocrScreenshot } = await performOcr( + targetTexts, + textxRange, + yRange, + cachedFrame, + 10, + 5 + ); + ocrScreenshots.push(ocrScreenshot); - // 4. 文本匹配与交互(双向匹配,增强容错性) let foundTarget = false; for (const targetText of targetTexts) { const targetResult = ocrResults.find(res => @@ -154,11 +174,9 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text ); if (!targetResult) continue; - // 计数防误触 const materialId = `${targetText}-${targetResult.y}`; recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1); - // Y轴对齐判断 const centerYTargetText = targetResult.y + targetResult.height / 2; if (Math.abs(centerYTargetText - (fRes.y + fRes.height / 2)) <= texttolerance) { if (recognitionCount.get(materialId) >= 1) { @@ -171,7 +189,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text } } - // 5. 未找到目标则翻滚(保留原逻辑) if (!foundTarget) { await keyMouseScript.runFile(`assets/滚轮下翻.json`); } @@ -179,20 +196,16 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text } catch (error) { log.error(`对齐交互异常: ${error.message}`); } finally { - // 6. 统一释放所有资源(新增:解决内存泄漏) - // 释放缓存帧 if (cachedFrame) { if (cachedFrame.Dispose) cachedFrame.Dispose(); else if (cachedFrame.dispose) cachedFrame.dispose(); } - // 释放OCR截图 for (const screenshot of ocrScreenshots) { if (screenshot) { if (screenshot.Dispose) screenshot.Dispose(); else if (screenshot.dispose) screenshot.dispose(); } } - // 任务状态日志(保留原逻辑) if (state.cancelRequested) { log.info("检测任务已取消"); } else if (!state.completed) { diff --git a/repo/js/背包材料统计/lib/backStats.js b/repo/js/背包材料统计/lib/backStats.js index 383c029b7..6d060f7cf 100644 --- a/repo/js/背包材料统计/lib/backStats.js +++ b/repo/js/背包材料统计/lib/backStats.js @@ -5,6 +5,9 @@ const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 75 const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711)); const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay); +// 全局图片缓存(避免重复加载) +const globalMaterialImageCache = {}; + // 配置参数 const pageScrollCount = 22; // 最多滑页次数 @@ -127,7 +130,22 @@ async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) { stepDistance, stepInterval: delayMs, waitBefore: 50, - waitAfter: 700, // 原逻辑中松开后等待700ms + waitAfter: 500, // 原逻辑中松开后等待700ms + repeat: 1 + }); +} + +// 回退翻页函数(用于后续优先级材料扫描) +async function scrollBackPage(totalDistance, stepDistance = 10, delayMs = 5) { + const backHoldY = 1080 - holdY; // 回退翻页的Y值 + await mouseDrag({ + holdMouseX: holdX, // 固定起点X + holdMouseY: backHoldY, // 回退翻页的Y值 + totalDistance: totalDistance, // 向下滑动(正值) + stepDistance, + stepInterval: delayMs, + waitBefore: 50, + waitAfter: 500, repeat: 1 }); } @@ -281,49 +299,47 @@ async function mouseDrag({ } function filterMaterialsByPriority(materialsCategory) { - // 获取当前材料分类的优先级 const currentPriority = materialPriority[materialsCategory]; if (currentPriority === undefined) { throw new Error(`Invalid materialsCategory: ${materialsCategory}`); } - // 获取当前材料分类的 materialTypeMap 对应值 const currentType = materialTypeMap[materialsCategory]; if (currentType === undefined) { throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`); } - // 获取所有优先级更低的材料分类(后位材料) - const backPriorityMaterials = Object.keys(materialPriority) - .filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType); + const allSameTypeMaterials = Object.keys(materialPriority) + .filter(mat => materialTypeMap[mat] === currentType); - // 合并当前和后位材料分类(只包含同位和后位,不包含前位) - // 只有同位或后位材料才会触发全列扫描 - const finalFilteredMaterials = [...backPriorityMaterials, materialsCategory]; + const finalFilteredMaterials = allSameTypeMaterials.sort((a, b) => materialPriority[a] - materialPriority[b]); return finalFilteredMaterials } // 扫描材料 -async function scanMaterials(materialsCategory, materialCategoryMap) { - // 材料图片缓存 - const materialImages = {}; // 用于缓存加载的图片 - const priorityMaterialImages = {}; // 用于缓存优先级材料图片 +async function scanMaterials(materialsCategory, materialCategoryMap, isPostPriority = false) { + // 使用全局图片缓存 + const materialImages = globalMaterialImageCache; + + const currentType = materialTypeMap[materialsCategory]; + const currentPriority = materialPriority[materialsCategory]; - // 获取当前+后位材料名单(仅包含同位和后位,不包含前位) const priorityMaterialNames = []; const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory); - for (const category of finalFilteredMaterials) { + + const currentTypeMaterials = finalFilteredMaterials.filter(category => materialTypeMap[category] === currentType); + + for (const category of currentTypeMaterials) { const materialIconDir = `assets/images/${category}`; const materialIconFilePaths = file.ReadPathSync(materialIconDir); for (const filePath of materialIconFilePaths) { - const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名 + const name = basename(filePath).replace(".png", ""); priorityMaterialNames.push({ category, name }); - // 预加载优先级材料图片 - if (!priorityMaterialImages[name]) { - const mat = file.readImageMatSync(filePath); + if (!materialImages[name]) { + const mat = getCachedImageMat(filePath); if (!mat.empty()) { - priorityMaterialImages[name] = mat; + materialImages[name] = mat; } } } @@ -352,7 +368,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) { continue; } - const mat = file.readImageMatSync(filePath); + const mat = getCachedImageMat(filePath); if (mat.empty()) { log.error(`加载图标失败:${filePath}`); continue; // 跳过当前文件 @@ -413,33 +429,119 @@ async function scanMaterials(materialsCategory, materialCategoryMap) { tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性 let phrasesStartTime = Date.now(); let previousScreenshot = null; // 用于存储上一次翻页前的截图 + + // 后续优先级材料扫描:滑条重置后先检查第八列是否有前位材料 + if (isPostPriority) { + log.info(`后续优先级材料扫描 - 检查第八列前位材料`); + const ra = captureGameRegion(); + + const lowerPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] < currentPriority); + log.info(`检查前位材料分类: ${lowerPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`); + const lowerPriorityMatches = []; + + for (const category of lowerPriorityMaterials) { + const categoryMaterials = priorityMaterialNames + .filter(({ name, category: cat }) => cat === category) + .map(({ name }) => { + const mat = materialImages[name]; + return mat ? { name, mat } : null; + }) + .filter(Boolean); + + if (categoryMaterials.length > 0) { + const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8); + const foundMaterials = matchResults.filter(r => r.result).map(r => r.name); + if (foundMaterials.length > 0) { + log.info(`第八列识别到前位材料 [${category}]: ${foundMaterials.join(', ')}`); + } + lowerPriorityMatches.push(...matchResults.filter(r => r.result)); + } + } + + log.info(`第八列前位材料总数: ${lowerPriorityMatches.length}`); + if (lowerPriorityMatches.length === 0) { + log.info(`未发现前位材料,回退一页`); + await scrollBackPage(totalPageDistance, 10, 5); + await sleep(500); + } else { + log.info(`发现前位材料,照常继续扫描`); + } + } + // 扫描背包中的材料 for (let scroll = 0; scroll <= pageScrollCount; scroll++) { const ra = captureGameRegion(); - // 重置foundPriorityMaterial标志,每次翻页都重新检查 foundPriorityMaterial = false; - // 检查第八列是否有目标材料 - // priorityMaterialNames只包含当前和后位材料 - const priorityMaterialsToMatch = priorityMaterialNames - .filter(({ name }) => !recognizedMaterials.has(name)) - .map(({ name }) => { - const mat = priorityMaterialImages[name]; - return mat ? { name, mat } : null; - }) - .filter(Boolean); + const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory); - if (priorityMaterialsToMatch.length > 0) { - const matchResults = await parallelTemplateMatch(ra, priorityMaterialsToMatch, 1142, startY, columnWidth, columnHeight, 0.8); + const currentTypeMaterials = finalFilteredMaterials.filter(category => materialTypeMap[category] === currentType); - for (const { name, result } of matchResults) { - if (result) { - foundPriorityMaterial = true; // 标记找到目标材料 - // log.info(`发现目标材料: ${name},开始全列扫描`); - break; + log.info(`第八列扫描 - 当前分类: ${materialsCategory}, 优先级: ${currentPriority}`); + + if (currentPriority <= 2) { + const lowerPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] < currentPriority); + log.info(`检查前位材料分类: ${lowerPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`); + const lowerPriorityMatches = []; + + for (const category of lowerPriorityMaterials) { + const categoryMaterials = priorityMaterialNames + .filter(({ name, category: cat }) => cat === category && !recognizedMaterials.has(name)) + .map(({ name }) => { + const mat = materialImages[name]; + return mat ? { name, mat } : null; + }) + .filter(Boolean); + + if (categoryMaterials.length > 0) { + const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8); + const foundMaterials = matchResults.filter(r => r.result).map(r => r.name); + if (foundMaterials.length > 0) { + log.info(`第八列识别到前位材料 [${category}]: ${foundMaterials.join(', ')}`); + } + lowerPriorityMatches.push(...matchResults.filter(r => r.result)); } } + + log.info(`第八列前位材料总数: ${lowerPriorityMatches.length}`); + if (lowerPriorityMatches.length < 4) { + log.info(`前位材料少于4张,触发全列扫描`); + foundPriorityMaterial = true; + } else { + log.info(`4张都是前位材料,继续翻页`); + } + } else { + const currentOrHigherPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] >= currentPriority); + log.info(`检查同位/后位材料分类: ${currentOrHigherPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`); + const currentOrHigherMatches = []; + + for (const category of currentOrHigherPriorityMaterials) { + const categoryMaterials = priorityMaterialNames + .filter(({ name, category: cat }) => cat === category && !recognizedMaterials.has(name)) + .map(({ name }) => { + const mat = materialImages[name]; + return mat ? { name, mat } : null; + }) + .filter(Boolean); + + if (categoryMaterials.length > 0) { + const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8); + const foundMaterials = matchResults.filter(r => r.result).map(r => r.name); + if (foundMaterials.length > 0) { + log.info(`第八列识别到同位/后位材料 [${category}]: ${foundMaterials.join(', ')}`); + } + currentOrHigherMatches.push(...matchResults.filter(r => r.result)); + } + } + + log.info(`第八列同位/后位材料总数: ${currentOrHigherMatches.length}`); + if (currentOrHigherMatches.length > 0) { + log.info(`发现同位/后位材料,触发全列扫描`); + foundPriorityMaterial = true; + } else { + log.info(`未发现同位/后位材料,继续翻页`); + } } // 只有发现目标材料时,才执行全列扫描 @@ -558,7 +660,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) { } // 滑动到下一页 - if (scroll < pageScrollCount) { + if (scroll < pageScrollCount && !shouldEndScan) { if (useScreenComparison && previousScreenshot) { // 使用模板匹配比较两次翻页前的截图(兜底机制) const matchRo = RecognitionObject.TemplateMatch( @@ -733,219 +835,232 @@ function dynamicMaterialGrouping(materialCategoryMap) { // 主逻辑函数 async function MaterialPath(materialCategoryMap, cachedFrame = null) { - - // 1. 先记录原始名称与别名的映射关系(用于最后反向转换) - const nameMap = new Map(); - Object.values(materialCategoryMap).flat().forEach(originalName => { - const aliasName = MATERIAL_ALIAS[originalName] || originalName; - nameMap.set(aliasName, originalName); // 存储:别名→原始名 - }); - - // 2. 转换materialCategoryMap为别名(用于内部处理) - const processedMap = {}; - Object.entries(materialCategoryMap).forEach(([category, names]) => { - processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name); - }); - materialCategoryMap = processedMap; - - const maxStage = 4; // 最大阶段数 - let stage = 0; // 当前阶段 - let currentGroupIndex = 0; // 当前处理的分组索引 - let currentCategoryIndex = 0; // 当前处理的分类索引 - 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("材料 动态[分组]结果:"); - sortedGroups.forEach(group => { - log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`); - }); - - let loopCount = 0; - const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环 - - while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 ===== - loopCount++; - switch (stage) { - case 0: // 返回主界面 - log.info("返回主界面"); - await genshin.returnMainUi(); - await sleep(500); - stage = 1; // 进入下一阶段 - break; - - case 1: // 打开背包界面 - // log.info("打开背包界面"); - keyPress("B"); // 打开背包界面 - await sleep(800); // 减少等待时间 - - cachedFrame?.dispose(); - cachedFrame = captureGameRegion(); - - const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000); - if (backpackResult.isDetected) { - // log.info("成功识别背包图标"); - stage = 2; // 进入下一阶段 - } else { - log.warn("未识别到背包图标,重新尝试"); - // ===== 补充优化:连续回退时释放资源 ===== - cachedFrame?.dispose(); - stage = 0; // 回退 - } - break; - - case 2: // 按分组处理材料分类 - if (currentGroupIndex < sortedGroups.length) { - const group = sortedGroups[currentGroupIndex]; - - if (currentCategoryIndex < group.categories.length) { - materialsCategory = group.categories[currentCategoryIndex]; - const offset = materialTypeMap[materialsCategory]; - const menuClickX = Math.round(575 + (offset - 1) * 96.25); - // log.info(`点击坐标 (${menuClickX},75)`); - click(menuClickX, 75); - - await sleep(500); - - cachedFrame?.dispose(); - cachedFrame = captureGameRegion(); - - stage = 3; // 进入下一阶段 - } else { - currentGroupIndex++; - currentCategoryIndex = 0; // 重置分类索引 - stage = 2; // 继续处理下一组 - } - } else { - stage = 5; // 跳出循环 - } - break; - - case 3: // 识别材料分类 - let CategoryObject = getCategoryObject(materialsCategory); - if (!CategoryObject) { - log.error("未知的材料分类"); - // ===== 补充优化:异常时释放资源并退出 ===== - cachedFrame?.dispose(); - stage = 0; // 回退到阶段0 - return; - } - - const CategoryResult = await recognizeImage(CategoryObject, cachedFrame); - if (CategoryResult.isDetected) { - log.info(`识别到${materialsCategory} 所在分类。`); - stage = 4; // 进入下一阶段 - } else { - log.warn("未识别到材料分类图标,重新尝试"); - // log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); - // ===== 补充优化:连续回退时释放资源 ===== - cachedFrame?.dispose(); - stage = 2; // 回退到阶段2 - } - break; - - case 4: // 扫描材料 - log.info("芭芭拉,冲鸭!"); - - // 判断是否需要重置滑条 - 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; - - case 5: // 所有分组处理完毕 - log.info("所有分组处理完毕,返回主界面"); - await genshin.returnMainUi(); - stage = maxStage + 1; // 确保退出循环 - break; - } - } - - // ===== 补充优化:循环超限处理,防止卡死 ===== - if (loopCount > maxLoopCount) { - log.error(`主循环次数超限(${maxLoopCount}次),强制退出`); - cachedFrame?.dispose(); - await genshin.returnMainUi(); - return []; - } - - await genshin.returnMainUi(); // 返回主界面 - log.info("扫描流程结束"); - - - // 3. 处理完成后,将输出结果转换回原始名称 - const finalResult = allLowCountMaterials.map(categoryMaterials => { - return categoryMaterials.map(material => { - // 假设material包含name属性,将别名转回原始名 - return { - ...material, - name: nameMap.get(material.name) || material.name // 反向映射 - }; + try { + // 1. 先记录原始名称与别名的映射关系(用于最后反向转换) + const nameMap = new Map(); + Object.values(materialCategoryMap).flat().forEach(originalName => { + const aliasName = MATERIAL_ALIAS[originalName] || originalName; + nameMap.set(aliasName, originalName); // 存储:别名→原始名 }); - }); - cachedFrame?.dispose(); - return finalResult; // 返回转换后的结果(如"晶蝶") + // 2. 转换materialCategoryMap为别名(用于内部处理) + const processedMap = {}; + Object.entries(materialCategoryMap).forEach(([category, names]) => { + processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name); + }); + materialCategoryMap = processedMap; + + const maxStage = 4; // 最大阶段数 + let stage = 0; // 当前阶段 + let currentGroupIndex = 0; // 当前处理的分组索引 + let currentCategoryIndex = 0; // 当前处理的分类索引 + 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("材料 动态[分组]结果:"); + sortedGroups.forEach(group => { + log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`); + }); + + let loopCount = 0; + const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环 + + while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 ===== + loopCount++; + switch (stage) { + case 0: // 返回主界面 + log.info("返回主界面"); + await genshin.returnMainUi(); + await sleep(500); + stage = 1; // 进入下一阶段 + break; + + case 1: // 打开背包界面 + // log.info("打开背包界面"); + keyPress("B"); // 打开背包界面 + state.ocrPaused = true; + log.info("背包扫描开始,已暂停OCR拾取任务"); + await sleep(800); // 减少等待时间 + + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + + const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000, 500, true, "背包"); + if (backpackResult.isDetected) { + // log.info("成功识别背包图标"); + stage = 2; // 进入下一阶段 + } else { + log.warn("未识别到背包图标,重新尝试"); + // ===== 补充优化:连续回退时释放资源 ===== + cachedFrame?.dispose(); + stage = 0; // 回退 + } + break; + + case 2: // 按分组处理材料分类 + if (currentGroupIndex < sortedGroups.length) { + const group = sortedGroups[currentGroupIndex]; + + if (currentCategoryIndex < group.categories.length) { + materialsCategory = group.categories[currentCategoryIndex]; + const offset = materialTypeMap[materialsCategory]; + const menuClickX = Math.round(575 + (offset - 1) * 96.25); + // log.info(`点击坐标 (${menuClickX},75)`); + click(menuClickX, 75); + + await sleep(500); + + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + + stage = 3; // 进入下一阶段 + } else { + currentGroupIndex++; + currentCategoryIndex = 0; // 重置分类索引 + stage = 2; // 继续处理下一组 + } + } else { + stage = 5; // 跳出循环 + } + break; + + case 3: // 识别材料分类 + let CategoryObject = getCategoryObject(materialsCategory); + if (!CategoryObject) { + log.error("未知的材料分类"); + // ===== 补充优化:异常时释放资源并退出 ===== + cachedFrame?.dispose(); + stage = 0; // 回退到阶段0 + return; + } + + const CategoryResult = await recognizeImage(CategoryObject, cachedFrame, 2000, 500, true, materialsCategory); + if (CategoryResult.isDetected) { + log.info(`识别到${materialsCategory} 所在分类。`); + stage = 4; // 进入下一阶段 + } else { + log.warn("未识别到材料分类图标,重新尝试"); + // log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); + // ===== 补充优化:连续回退时释放资源 ===== + cachedFrame?.dispose(); + stage = 2; // 回退到阶段2 + } + break; + + case 4: // 扫描材料 + log.info("芭芭拉,冲鸭!"); + + // 判断是否需要重置滑条 + if (!skipSliderReset) { + await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端 + await sleep(200); + leftButtonDown(); // 长按左键重置材料滑条 + await sleep(300); + leftButtonUp(); + await sleep(200); + } else { + log.info("同一大类且为后位材料,跳过滑条重置"); + // 不重置滑条,直接从当前位置开始检查第八列 + } + + // 判断是否是后续优先级材料(优先级高于前一个分类) + const currentPriority = materialPriority[materialsCategory]; + const isPostPriority = prevPriority !== null && currentPriority > prevPriority; + if (isPostPriority) { + log.info(`后续优先级材料扫描 - 当前优先级: ${currentPriority}, 前位优先级: ${prevPriority}`); + } + + // 扫描材料并获取低于目标数量的材料 + const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap, isPostPriority); + 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; + + case 5: // 所有分组处理完毕 + log.info("所有分组处理完毕,返回主界面"); + await genshin.returnMainUi(); + stage = maxStage + 1; // 确保退出循环 + break; + } + } + + // ===== 补充优化:循环超限处理,防止卡死 ===== + if (loopCount > maxLoopCount) { + log.error(`主循环次数超限(${maxLoopCount}次),强制退出`); + cachedFrame?.dispose(); + await genshin.returnMainUi(); + return []; + } + + await genshin.returnMainUi(); // 返回主界面 + log.info("扫描流程结束"); + + + // 3. 处理完成后,将输出结果转换回原始名称 + const finalResult = allLowCountMaterials.map(categoryMaterials => { + return categoryMaterials.map(material => { + // 假设material包含name属性,将别名转回原始名 + return { + ...material, + name: nameMap.get(material.name) || material.name // 反向映射 + }; + }); + }); + + cachedFrame?.dispose(); + return finalResult; // 返回转换后的结果(如"晶蝶") + } finally { + state.ocrPaused = false; + log.info("背包扫描结束,已恢复OCR拾取任务"); + } } diff --git a/repo/js/背包材料统计/lib/imageClick.js b/repo/js/背包材料统计/lib/imageClick.js index 8e514eec1..eb019618c 100644 --- a/repo/js/背包材料统计/lib/imageClick.js +++ b/repo/js/背包材料统计/lib/imageClick.js @@ -217,11 +217,36 @@ async function imageClickBackgroundTask() { // 配置参数 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 !== ""); + let specificNames = []; + try { + specificNames = Array.from(settings.PopupNames || []); + } catch (e) { + log.error(`获取PopupNames设置失败: ${e.message}`); + } + + let availablePopupDirs = []; + try { + const imageClickDir = "assets/imageClick"; + const subDirs = readAllFilePaths(imageClickDir, 0, 2, [], true); + availablePopupDirs = subDirs.filter(subDir => { + const dirName = basename(subDir); + const entries = readAllFilePaths(subDir, 0, 0, [], true); + return entries.some(entry => normalizePath(entry).endsWith('/icon')); + }).map(dir => basename(dir)); + log.info(`可用弹窗目录:${availablePopupDirs.join(', ')}`); + } catch (e) { + log.error(`扫描弹窗目录失败: ${e.message}`); + } + + if (specificNames.length === 0) { + log.info("未指定弹窗名称,将处理所有可用弹窗"); + } else { + const invalidNames = specificNames.filter(name => !availablePopupDirs.includes(name)); + if (invalidNames.length > 0) { + log.warn(`以下弹窗名称不存在,将被忽略:${invalidNames.join(', ')}`); + specificNames = specificNames.filter(name => availablePopupDirs.includes(name)); + } + } // 预加载资源 const preloadedResources = await preloadImageResources(specificNames); diff --git a/repo/js/背包材料统计/lib/updateSettings.js b/repo/js/背包材料统计/lib/updateSettings.js new file mode 100644 index 000000000..9b8f4b12a --- /dev/null +++ b/repo/js/背包材料统计/lib/updateSettings.js @@ -0,0 +1,180 @@ +// 自动更新settings.json中的options数组 +// 使用BetterGI的file对象,不需要Node.js的fs模块 + +var SETTINGS_FILE = "settings.json"; + +function updateSettingsOptions() { + log.info("开始更新settings.json..."); + try { + var settingsContent = file.readTextSync(SETTINGS_FILE); + log.info("settings.json内容长度: " + settingsContent.length); + var settings = JSON.parse(settingsContent); + log.info("settings.json解析成功,配置项数量: " + settings.length); + + var hasChanges = false; + + var popupDirs = readAllFilePaths("assets/imageClick", 0, 2, [], true) + .filter(function (dirPath) { + var entries = readAllFilePaths(dirPath, 0, 0, [], true); + return entries.some(function (entry) { + return normalizePath(entry).endsWith('/icon'); + }); + }) + .filter(function (dirPath) { + return !normalizePath(dirPath).includes('/其他/'); + }) + .map(function (dirPath) { + return basename(dirPath); + }) + .sort(); + log.info("扫描到弹窗目录数量: " + popupDirs.length); + + var popupSetting = settings.find(function (s) { + return s.name === "PopupNames"; + }); + if (popupSetting) { + log.info("找到PopupNames配置项"); + var existingOptions = popupSetting.options || []; + log.info("现有options数量: " + existingOptions.length); + + var existingSet = {}; + for (var k = 0; k < existingOptions.length; k++) { + existingSet[existingOptions[k]] = true; + } + + var popupSet = {}; + for (var p = 0; p < popupDirs.length; p++) { + popupSet[popupDirs[p]] = true; + } + + var newOptions = []; + var removedOptions = []; + for (var m = 0; m < popupDirs.length; m++) { + if (!existingSet[popupDirs[m]]) { + newOptions.push(popupDirs[m]); + } + } + for (var n = 0; n < existingOptions.length; n++) { + if (!popupSet[existingOptions[n]]) { + removedOptions.push(existingOptions[n]); + } + } + + log.info("新增options数量: " + newOptions.length); + log.info("删除options数量: " + removedOptions.length); + + if (newOptions.length > 0 || removedOptions.length > 0) { + popupSetting.options = popupDirs; + hasChanges = true; + if (newOptions.length > 0) { + log.info("PopupNames新增选项: " + newOptions.join(', ')); + } + if (removedOptions.length > 0) { + log.info("PopupNames删除选项: " + removedOptions.join(', ')); + } + } else { + log.info("PopupNames无新增选项"); + } + } else { + log.info("未找到PopupNames配置项"); + } + + var cdCategories = readAllFilePaths("materialsCD", 0, 1, ['.txt']) + .map(function (filePath) { + return basename(filePath).replace('.txt', ''); + }) + .sort(); + + var cdSetting = settings.find(function (s) { + return s.name === "CDCategories"; + }); + if (cdSetting) { + var existingOptions = cdSetting.options || []; + var existingSet = {}; + for (var k = 0; k < existingOptions.length; k++) { + existingSet[existingOptions[k]] = true; + } + var cdSet = {}; + for (var p = 0; p < cdCategories.length; p++) { + cdSet[cdCategories[p]] = true; + } + var newOptions = []; + var removedOptions = []; + for (var m = 0; m < cdCategories.length; m++) { + if (!existingSet[cdCategories[m]]) { + newOptions.push(cdCategories[m]); + } + } + for (var n = 0; n < existingOptions.length; n++) { + if (!cdSet[existingOptions[n]]) { + removedOptions.push(existingOptions[n]); + } + } + if (newOptions.length > 0 || removedOptions.length > 0) { + cdSetting.options = cdCategories; + hasChanges = true; + if (newOptions.length > 0) { + log.info("CDCategories新增选项: " + newOptions.join(', ')); + } + if (removedOptions.length > 0) { + log.info("CDCategories删除选项: " + removedOptions.join(', ')); + } + } + } + + var pickCategories = readAllFilePaths("targetText", 0, 1, ['.txt']) + .map(function (filePath) { + return basename(filePath).replace('.txt', ''); + }) + .sort(); + + var pickSetting = settings.find(function (s) { + return s.name === "PickCategories"; + }); + if (pickSetting) { + var existingOptions = pickSetting.options || []; + var existingSet = {}; + for (var k = 0; k < existingOptions.length; k++) { + existingSet[existingOptions[k]] = true; + } + var pickSet = {}; + for (var p = 0; p < pickCategories.length; p++) { + pickSet[pickCategories[p]] = true; + } + var newOptions = []; + var removedOptions = []; + for (var m = 0; m < pickCategories.length; m++) { + if (!existingSet[pickCategories[m]]) { + newOptions.push(pickCategories[m]); + } + } + for (var n = 0; n < existingOptions.length; n++) { + if (!pickSet[existingOptions[n]]) { + removedOptions.push(existingOptions[n]); + } + } + if (newOptions.length > 0 || removedOptions.length > 0) { + pickSetting.options = pickCategories; + hasChanges = true; + if (newOptions.length > 0) { + log.info("PickCategories新增选项: " + newOptions.join(', ')); + } + if (removedOptions.length > 0) { + log.info("PickCategories删除选项: " + removedOptions.join(', ')); + } + } + } + + if (hasChanges) { + var updatedContent = JSON.stringify(settings, null, 2); + file.writeTextSync(SETTINGS_FILE, updatedContent, false); + log.info("settings.json已自动更新"); + } else { + log.info("settings.json无需更新"); + } + } catch (error) { + log.error("自动更新settings.json失败: " + error.message); + } +} + +updateSettingsOptions(); diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js index b141cf57b..dc2f65afd 100644 --- a/repo/js/背包材料统计/main.js +++ b/repo/js/背包材料统计/main.js @@ -1,3 +1,48 @@ +// ============================================== +// 常量与配置(集中管理硬编码值) +// ============================================== +const CONSTANTS = { + // 路径与目录配置 + MATERIAL_CD_DIR: "materialsCD", + TARGET_TEXT_DIR: "targetText", + PATHING_DIR: "pathing", + RECORD_DIR: "pathing_record", + 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: "[初始化]", + PATH: "[路径处理]", + MATERIAL: "[材料管理]", + MONSTER: "[怪物映射]", + CD: "[CD控制]", + RECORD: "[记录管理]", + MAIN: "[主流程]" + } +}; + +// ============================================== +// 引入外部脚本(源码不变) +// ============================================== +eval(file.readTextSync("lib/file.js")); +eval(safeReadTextSync("lib/updateSettings.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")); + // ============================================== // 内容检测码生成(通用哈希逻辑) // ============================================== @@ -69,54 +114,31 @@ function generatePathContentCode(pathingFilePath) { } } -// ============================================== -// 常量与配置(集中管理硬编码值) -// ============================================== -const CONSTANTS = { - // 路径与目录配置 - MATERIAL_CD_DIR: "materialsCD", - TARGET_TEXT_DIR: "targetText", - PATHING_DIR: "pathing", - RECORD_DIR: "pathing_record", - 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: "[初始化]", - PATH: "[路径处理]", - MATERIAL: "[材料管理]", - MONSTER: "[怪物映射]", - CD: "[CD控制]", - RECORD: "[记录管理]", - MAIN: "[主流程]" - } -}; - -// ============================================== -// 引入外部脚本(源码不变) -// ============================================== -eval(file.readTextSync("lib/file.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")); - // ============================================== // 全局状态(保持不变) // ============================================== -var state = { completed: false, cancelRequested: false }; +var state = { completed: false, cancelRequested: false, ocrPaused: false }; + +// ============================================== +// 全局图片缓存(避免重复加载) +// ============================================== +const globalImageCache = new Map(); + +function getCachedImageMat(filePath) { + if (globalImageCache.has(filePath)) { + return globalImageCache.get(filePath); + } + const mat = file.readImageMatSync(filePath); + if (!mat.empty()) { + globalImageCache.set(filePath, mat); + } + return mat; +} + +// ============================================== +// OCR上下文(用于动态过滤拾取列表) +// ============================================== +const ocrContext = { currentPathType: null, currentTargetMaterials: [], pathingMonsterMaterials: new Set() }; // ============================================== // 初始化配置参数 @@ -124,17 +146,34 @@ var state = { completed: false, cancelRequested: false }; const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); const notify = settings.notify || false; const noRecord = settings.noRecord || false; +const debugLog = settings.debugLog || 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) || 9000))); // 设定的超量目标数量 const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null; // 解析需要处理的CD分类 -const allowedCDCategories = (settings.CDCategories || "") - .split(/[,,、 \s]+/) - .map(cat => cat.trim()) - .filter(cat => cat !== ""); +let allowedCDCategories = []; +try { + allowedCDCategories = Array.from(settings.CDCategories || []); +} catch (e) { + log.error(`${CONSTANTS.LOG_MODULES.INIT}获取CDCategories设置失败: ${e.message}`); +} + +let availableCDCategories = []; +try { + const cdFilePaths = readAllFilePaths(CONSTANTS.MATERIAL_CD_DIR, 0, 1, ['.txt']); + availableCDCategories = cdFilePaths.map(filePath => basename(filePath).replace('.txt', '')); + log.info(`${CONSTANTS.LOG_MODULES.INIT}可用CD分类:${availableCDCategories.join(', ')}`); +} catch (e) { + log.error(`${CONSTANTS.LOG_MODULES.INIT}扫描CD目录失败: ${e.message}`); +} if (allowedCDCategories.length > 0) { + const invalidCategories = allowedCDCategories.filter(cat => !availableCDCategories.includes(cat)); + if (invalidCategories.length > 0) { + log.warn(`${CONSTANTS.LOG_MODULES.INIT}以下CD分类不存在,将被忽略:${invalidCategories.join('、')}`); + allowedCDCategories = allowedCDCategories.filter(cat => availableCDCategories.includes(cat)); + } log.info(`${CONSTANTS.LOG_MODULES.INIT}已配置只处理以下CD分类:${allowedCDCategories.join('、')}`); } else { log.info(`${CONSTANTS.LOG_MODULES.INIT}未配置CD分类过滤,将处理所有分类`); @@ -150,6 +189,7 @@ const material_mapping = { "经验书、怪物掉落": "怪物掉落素材", "一般素材": "一般素材", "采集食物": "采集食物", + "周本素材": "周本素材", "烹饪用食材": "烹饪食材", "世界BOSS": "角色突破素材", "木材": "木材", @@ -199,19 +239,16 @@ parseMonsterMaterials(); // 初始化怪物材料映射 // ============================================== // 路径模式配置 // ============================================== -const pathingValue = settings.Pathing || ''; -// 从选择字符串中提取前缀数字,适配新的settings.json结构 -const pathingPrefix = pathingValue.match(/^(\d+)/) ? pathingValue.match(/^(\d+)/)[1] : ''; +const pathingValue = Array.from(settings.Pathing || []); const pathingMode = { - includeBoth: pathingPrefix === "1", - onlyPathing: pathingPrefix === "2", - onlyCategory: pathingPrefix === "3" + includeBoth: pathingValue.length === 2, + onlyPathing: pathingValue.length === 1 && pathingValue.includes('📁pathing材料'), + onlyCategory: pathingValue.length === 1 && pathingValue.includes('【扫描额外的分类】') }; -const isInvalidMode = !pathingMode.includeBoth && !pathingMode.onlyPathing && !pathingMode.onlyCategory; -if (isInvalidMode) { - log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到无效的Pathing设置(${pathingValue}),自动切换为【路径材料】专注模式`); +if (pathingValue.length === 0) { + log.warn(`${CONSTANTS.LOG_MODULES.PATH}未配置Pathing,默认为仅📁pathing材料`); pathingMode.onlyPathing = true; } @@ -532,7 +569,7 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco const normalContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; try { - if (runTime >= 3) { // 运行时间≥3秒才处理记录 + if (runTime > 5) { // 运行时间>5秒才处理记录 // 怪物路径专用逻辑(判断对应材料总数量是否为0) const isMonsterPath = monsterToMaterials.hasOwnProperty(resourceName); // 是否为怪物路径 if (isMonsterPath) { @@ -564,18 +601,18 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco // 正常记录生成逻辑 const hasZeroMaterial = Object.values(materialCountDifferences).includes(0); - const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0; + const isFinalCumulativeDistanceTooSmall = finalCumulativeDistance <= 5; - if (!(hasZeroMaterial && isFinalCumulativeDistanceZero)) { + if (!(hasZeroMaterial && isFinalCumulativeDistanceTooSmall)) { writeContentToFile(recordPath, normalContent); log.info(`${CONSTANTS.LOG_MODULES.RECORD}正常记录已写入: ${recordPath}`); } else { if (hasZeroMaterial) log.warn(`${CONSTANTS.LOG_MODULES.RECORD}存在材料数目为0的情况: ${JSON.stringify(materialCountDifferences)}`); - if (isFinalCumulativeDistanceZero) log.warn(`${CONSTANTS.LOG_MODULES.RECORD}累计距离为0: finalCumulativeDistance=${finalCumulativeDistance}`); + if (isFinalCumulativeDistanceTooSmall) log.warn(`${CONSTANTS.LOG_MODULES.RECORD}累计距离≤5: finalCumulativeDistance=${finalCumulativeDistance}`); log.warn(`${CONSTANTS.LOG_MODULES.RECORD}未写入正常记录: ${recordPath}`); } } else { - log.warn(`${CONSTANTS.LOG_MODULES.RECORD}运行时间小于3秒,未满足记录条件: ${recordPath}`); + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}运行时间小于5秒,未满足记录条件: ${recordPath}`); } } catch (error) { log.error(`${CONSTANTS.LOG_MODULES.RECORD}记录运行时间失败: ${error}`); @@ -1028,7 +1065,7 @@ function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) { // 提取所有需要扫描的材料(超量+低数量共用同一源) const allMaterials = Object.values(materialCategoryMap).flat(); - log.info(`【材料基准】本次需扫描的全量材料:${allMaterials.join("、")}`); + if (debugLog) log.info(`【材料基准】本次需扫描的全量材料:${allMaterials.join("、")}`); // ========== 第一步:平行判断超量材料(原始数据,不经过低数量过滤) ========== pathingMaterialCounts.forEach(item => { @@ -1072,7 +1109,7 @@ function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) { // ========== 第三步:更新全局超量名单(去重) ========== excessMaterialNames = [...new Set(tempExcess)]; log.info(`【超量材料更新】共${excessMaterialNames.length}种:${excessMaterialNames.join("、")}`); - log.info(`【低数量材料】筛选后共${filteredLowCountMaterials.length}种:${filteredLowCountMaterials.map(m => m.name).join("、")}`); + if (debugLog) log.info(`【低数量材料】筛选后共${filteredLowCountMaterials.length}种:${filteredLowCountMaterials.map(m => m.name).join("、")}`); // 返回低数量材料(超量名单已独立生成) return filteredLowCountMaterials; @@ -1104,114 +1141,130 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir, const pathName = basename(pathingFilePath); const { foodExpAccumulator, currentMaterialName: prevMaterialName } = accumulators; - // ========== 1. CD 冷却校验 ========== - let refreshCD = null; - for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { - if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; - for (const [cdKey, cdItems] of Object.entries(cdInfo)) { - if (cdItems.includes(resourceName)) { - refreshCD = JSON.parse(cdKey); - break; + let startTime = null; + let initialPosition = null; + let finalPosition = null; + let runTime = 0; + let finalCumulativeDistance = 0; + + try { + // ========== 1. CD 冷却校验 ========== + let refreshCD = null; + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [cdKey, cdItems] of Object.entries(cdInfo)) { + if (cdItems.includes(resourceName)) { + refreshCD = JSON.parse(cdKey); + break; + } + } + if (refreshCD) break; + } + + if (!refreshCD) { + log.debug(`${CONSTANTS.LOG_MODULES.CD}狗粮材料【${resourceName}】未找到CD配置,跳过路径:${pathName}`); + await sleep(1); + return accumulators; + } + + // ========== 2. 路径0记录频率校验 ========== + const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir, true); + if (!isPathValid) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 0记录频率超限,跳过`); + await sleep(1); + return accumulators; + } + + // ========== 3. 时间成本校验 ========== + const currentTime = getCurrentTimeInHours(); + const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); + const perTime = noRecord ? null : calculatePerTime( + resourceName, + pathName, + recordDir, + noRecordDir, + true, + pathRecordCache, + pathingFilePath + ); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 单位EXP耗时:${perTime ?? '忽略'}秒/EXP`); + + const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir); + log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 预计耗时:${estimatedTime}秒`); + + const canRun = canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) + && isPathValid + && (noRecord || perTime === null || perTime <= timeCost); + + if (!canRun) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 不符合运行条件`); + await sleep(1); + return accumulators; + } + + let currentMaterialName = prevMaterialName; + if (currentMaterialName !== resourceName) { + if (prevMaterialName && foodExpAccumulator[prevMaterialName]) { + const prevMsg = `材料[${prevMaterialName}]收集完成,累计EXP:${foodExpAccumulator[prevMaterialName]}`; + sendNotificationInChunks(prevMsg, notification.Send); + } + currentMaterialName = resourceName; + foodExpAccumulator[resourceName] = 0; + log.info(`${CONSTANTS.LOG_MODULES.PATH}切换至狗粮材料【${resourceName}】`); + } + + startTime = new Date().toLocaleString(); + initialPosition = genshin.getPositionFromMap(); + await runPathAndNotify(pathingFilePath, currentMaterialName); + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const { success, totalExp } = await executeSalvageWithOCR(); + foodExpAccumulator[resourceName] += totalExp; + + const recordDirFinal = noRecord ? noRecordDir : recordDir; + const foodRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n移动距离: ${finalCumulativeDistance.toFixed(2)}\n分解状态: ${success ? "成功" : "失败"}\n本次EXP获取: ${totalExp}\n累计EXP获取: ${foodExpAccumulator[resourceName]}\n\n`; + + const canRecord = runTime > 5 && finalCumulativeDistance > 5; + if (canRecord) { + if (totalExp === 0) { + const zeroExpFilePath = `${recordDirFinal}/${resourceName}${CONSTANTS.FOOD_ZERO_EXP_SUFFIX}`; + writeContentToFile(zeroExpFilePath, foodRecordContent); + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}狗粮路径${pathName} EXP=0,写入0记录文件:${zeroExpFilePath}`); + } else { + const normalExpFilePath = `${recordDirFinal}/${resourceName}${CONSTANTS.FOOD_EXP_RECORD_SUFFIX}`; + writeContentToFile(normalExpFilePath, foodRecordContent); + const foodMsg = `狗粮路径【${pathName}】执行完成\n耗时:${runTime.toFixed(1)}秒\n本次EXP:${totalExp}\n累计EXP:${foodExpAccumulator[resourceName]}`; + sendNotificationInChunks(foodMsg, notification.Send); + } + } else { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}狗粮路径${pathName} 不满足记录条件:运行时间${runTime.toFixed(1)}秒(需>5秒)| 移动距离${finalCumulativeDistance.toFixed(2)}(需>5)`); + } + + await sleep(1); + return { ...accumulators, foodExpAccumulator, currentMaterialName }; + } catch (error) { + if (startTime && initialPosition) { + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const canRecord = runTime > 5 && finalCumulativeDistance > 5; + if (canRecord) { + const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; + const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${error.message}\n\n`; + writeContentToFile(`${CONSTANTS.NO_RECORD_DIR}/${resourceName}.txt`, noRecordContent); + log.info(`${CONSTANTS.LOG_MODULES.RECORD}已将错误路径记录为noRecord模式:${pathName}(实际执行:${runTime.toFixed(1)}秒,${finalCumulativeDistance.toFixed(2)}米)`); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}执行异常但不满足记录条件(运行时间${runTime.toFixed(1)}秒,移动距离${finalCumulativeDistance.toFixed(2)}米),跳过noRecord记录`); } } - if (refreshCD) break; + throw error; } - - if (!refreshCD) { - log.debug(`${CONSTANTS.LOG_MODULES.CD}狗粮材料【${resourceName}】未找到CD配置,跳过路径:${pathName}`); - await sleep(1); - return accumulators; - } - - // ========== 2. 路径0记录频率校验 ========== - const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir, true); // true=狗粮路径 - if (!isPathValid) { - log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 0记录频率超限,跳过`); - await sleep(1); - return accumulators; - } - - // ========== 3. 时间成本校验 ========== - const currentTime = getCurrentTimeInHours(); - const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); - const perTime = noRecord ? null : calculatePerTime( - resourceName, - pathName, - recordDir, - noRecordDir, - true, // 标记为狗粮路径 - pathRecordCache, - pathingFilePath - ); - - log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 单位EXP耗时:${perTime ?? '忽略'}秒/EXP`); - - // 计算并输出预计耗时 - const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir); - log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 预计耗时:${estimatedTime}秒`); - - // 校验是否满足运行条件(CD + 0记录频率 + 时间成本) - const canRun = canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) - && isPathValid - && (noRecord || perTime === null || perTime <= timeCost); - - if (!canRun) { - log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 不符合运行条件`); - await sleep(1); - return accumulators; - } - - // 切换目标材料 - let currentMaterialName = prevMaterialName; - if (currentMaterialName !== resourceName) { - if (prevMaterialName && foodExpAccumulator[prevMaterialName]) { - const prevMsg = `材料[${prevMaterialName}]收集完成,累计EXP:${foodExpAccumulator[prevMaterialName]}`; - sendNotificationInChunks(prevMsg, notification.Send); - } - currentMaterialName = resourceName; - foodExpAccumulator[resourceName] = 0; - log.info(`${CONSTANTS.LOG_MODULES.PATH}切换至狗粮材料【${resourceName}】`); - } - - // 执行路径 - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await runPathAndNotify(pathingFilePath, currentMaterialName); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; - - // 处理分解与EXP统计 - const { success, totalExp } = await executeSalvageWithOCR(); - foodExpAccumulator[resourceName] += totalExp; - - // ========== 4. 运行时间≥3秒 + 累计距离>0 才记录 ========== - const recordDirFinal = noRecord ? noRecordDir : recordDir; - const foodRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n移动距离: ${finalCumulativeDistance.toFixed(2)}\n分解状态: ${success ? "成功" : "失败"}\n本次EXP获取: ${totalExp}\n累计EXP获取: ${foodExpAccumulator[resourceName]}\n\n`; - - // 记录校验条件 - const canRecord = runTime >= 3 && finalCumulativeDistance > 0; - if (canRecord) { - if (totalExp === 0) { - // 情况1:EXP=0 → 写入专属0记录文件(_狗粮-0.txt),且先检查文件是否存在 - const zeroExpFilePath = `${recordDirFinal}/${resourceName}${CONSTANTS.FOOD_ZERO_EXP_SUFFIX}`; - writeContentToFile(zeroExpFilePath, foodRecordContent); - log.warn(`${CONSTANTS.LOG_MODULES.RECORD}狗粮路径${pathName} EXP=0,写入0记录文件:${zeroExpFilePath}`); - } else { - // 情况2:EXP>0 → 写入普通狗粮记录文件(_狗粮.txt) - const normalExpFilePath = `${recordDirFinal}/${resourceName}${CONSTANTS.FOOD_EXP_RECORD_SUFFIX}`; - writeContentToFile(normalExpFilePath, foodRecordContent); - // 发送有效运行通知 - const foodMsg = `狗粮路径【${pathName}】执行完成\n耗时:${runTime.toFixed(1)}秒\n本次EXP:${totalExp}\n累计EXP:${foodExpAccumulator[resourceName]}`; - sendNotificationInChunks(foodMsg, notification.Send); - } - } else { - log.warn(`${CONSTANTS.LOG_MODULES.RECORD}狗粮路径${pathName} 不满足记录条件:运行时间${runTime.toFixed(1)}秒(需≥3秒)| 移动距离${finalCumulativeDistance.toFixed(2)}(需>0)`); - } - - await sleep(1); - return { ...accumulators, foodExpAccumulator, currentMaterialName }; } /** @@ -1231,156 +1284,175 @@ async function processMonsterPathEntry(entry, context) { pathRecordCache } = context; - // 检查怪物材料是否全部超量 - const monsterMaterials = monsterToMaterials[monsterName] || []; - const allExcess = monsterMaterials.every(mat => excessMaterialNames.includes(mat)); - if (allExcess) { - log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】所有材料已超量,跳过路径:${pathName}`); - await sleep(1); - return context; - } + let startTime = null; + let initialPosition = null; + let finalPosition = null; + let runTime = 0; + let finalCumulativeDistance = 0; - // 用怪物名查CD - let refreshCD = null; - for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { - if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; - for (const [cdKey, cdItems] of Object.entries(cdInfo)) { - if (cdItems.includes(monsterName)) { - refreshCD = JSON.parse(cdKey); - break; + try { + const monsterMaterials = monsterToMaterials[monsterName] || []; + const allExcess = monsterMaterials.every(mat => excessMaterialNames.includes(mat)); + if (allExcess) { + log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】所有材料已超量,跳过路径:${pathName}`); + await sleep(1); + return context; + } + + let refreshCD = null; + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [cdKey, cdItems] of Object.entries(cdInfo)) { + if (cdItems.includes(monsterName)) { + refreshCD = JSON.parse(cdKey); + break; + } } - } - if (refreshCD) break; - } - - if (!refreshCD) { - log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】未找到CD配置,跳过路径:${pathName}`); - await sleep(1); - return context; - } - - // 检查是否可运行 - const currentTime = getCurrentTimeInHours(); - const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir, pathingFilePath); - const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir); - const perTime = noRecord ? null : calculatePerTime( - monsterName, - pathName, - recordDir, - noRecordDir, - false, - pathRecordCache, - pathingFilePath - ); - - log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); - - // 计算并输出预计耗时 - const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, monsterName }, recordDir, noRecordDir); - log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 预计耗时:${estimatedTime}秒`); - - if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { - log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 不符合运行条件`); - await sleep(1); - return context; - } - - // 构建怪物掉落材料的分类映射(用于扫描) - const resourceCategoryMap = {}; - const materials = monsterToMaterials[monsterName] || []; - materials.forEach(mat => { - const category = matchImageAndGetCategory(mat, imagesDir); - if (category) { - if (!resourceCategoryMap[category]) resourceCategoryMap[category] = []; - if (!resourceCategoryMap[category].includes(mat)) { - resourceCategoryMap[category].push(mat); - } - } - }); - log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物${monsterName}的扫描分类:${JSON.stringify(resourceCategoryMap)}`); - - // 处理运行逻辑 - let currentMaterialName = prevMaterialName; - let updatedFlattened = flattenedLowCountMaterials; - - if (noRecord) { - // noRecord模式 - if (currentMaterialName !== monsterName) { - currentMaterialName = monsterName; - materialAccumulatedDifferences[monsterName] = {}; - log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至怪物【${monsterName}】`); + if (refreshCD) break; } - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await runPathAndNotify(pathingFilePath, currentMaterialName); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; - - // 生成内容检测码 - const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; - const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; - writeContentToFile(`${noRecordDir}/${monsterName}.txt`, noRecordContent); - } else { - // 普通记录模式 - if (currentMaterialName !== monsterName) { - if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { - const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; - sendNotificationInChunks(prevMsg, notification.Send); - } - currentMaterialName = monsterName; - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - updatedFlattened = updatedLowCountMaterials - .flat() - .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); - materialAccumulatedDifferences[monsterName] = {}; + if (!refreshCD) { + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】未找到CD配置,跳过路径:${pathName}`); + await sleep(1); + return context; } - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await runPathAndNotify(pathingFilePath, currentMaterialName); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + const currentTime = getCurrentTimeInHours(); + const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir, pathingFilePath); + const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir); + const perTime = noRecord ? null : calculatePerTime( + monsterName, + pathName, + recordDir, + noRecordDir, + false, + pathRecordCache, + pathingFilePath + ); - // 计算材料变化 - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); + log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); - const materialCountDifferences = {}; - flattenedUpdated.forEach(updated => { - const original = updatedFlattened.find(m => m.name === updated.name); - if (original) { - const diff = parseInt(updated.count) - parseInt(original.count); - if (diff !== 0 || updated.name === updated.name) { - materialCountDifferences[updated.name] = diff; - globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; - materialAccumulatedDifferences[monsterName][updated.name] = (materialAccumulatedDifferences[monsterName][updated.name] || 0) + diff; + const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, monsterName }, recordDir, noRecordDir); + log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 预计耗时:${estimatedTime}秒`); + + if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 不符合运行条件`); + await sleep(1); + return context; + } + + const resourceCategoryMap = {}; + const materials = monsterToMaterials[monsterName] || []; + + ocrContext.currentPathType = 'monster'; + ocrContext.currentTargetMaterials = materials; + + materials.forEach(mat => { + const category = matchImageAndGetCategory(mat, imagesDir); + if (category) { + if (!resourceCategoryMap[category]) resourceCategoryMap[category] = []; + if (!resourceCategoryMap[category].includes(mat)) { + resourceCategoryMap[category].push(mat); } } }); + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物${monsterName}的扫描分类:${JSON.stringify(resourceCategoryMap)}`); - // 更新材料计数缓存 - updatedFlattened = updatedFlattened.map(m => { - const updated = flattenedUpdated.find(u => u.name === m.name); - return updated ? { ...m, count: updated.count } : m; - }); + let currentMaterialName = prevMaterialName; + let updatedFlattened = flattenedLowCountMaterials; - log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}怪物路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); - recordRunTime(monsterName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + if (noRecord) { + if (currentMaterialName !== monsterName) { + currentMaterialName = monsterName; + materialAccumulatedDifferences[monsterName] = {}; + log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至怪物【${monsterName}】`); + } + + startTime = new Date().toLocaleString(); + initialPosition = genshin.getPositionFromMap(); + await runPathAndNotify(pathingFilePath, currentMaterialName); + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; + const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; + writeContentToFile(`${noRecordDir}/${monsterName}.txt`, noRecordContent); + } else { + if (currentMaterialName !== monsterName) { + if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { + const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; + sendNotificationInChunks(prevMsg, notification.Send); + } + currentMaterialName = monsterName; + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + updatedFlattened = updatedLowCountMaterials + .flat() + .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); + materialAccumulatedDifferences[monsterName] = {}; + } + + startTime = new Date().toLocaleString(); + initialPosition = genshin.getPositionFromMap(); + await runPathAndNotify(pathingFilePath, currentMaterialName); + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); + + const materialCountDifferences = {}; + flattenedUpdated.forEach(updated => { + const original = updatedFlattened.find(m => m.name === updated.name); + if (original) { + const diff = parseInt(updated.count) - parseInt(original.count); + if (diff !== 0 || materials.includes(updated.name)) { + materialCountDifferences[updated.name] = diff; + globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; + materialAccumulatedDifferences[monsterName][updated.name] = (materialAccumulatedDifferences[monsterName][updated.name] || 0) + diff; + } + } + }); + + updatedFlattened = updatedFlattened.map(m => { + const updated = flattenedUpdated.find(u => u.name === m.name); + return updated ? { ...m, count: updated.count } : m; + }); + + log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}怪物路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); + recordRunTime(monsterName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + } + + await sleep(1); + return { + ...context, + currentMaterialName, + flattenedLowCountMaterials: updatedFlattened, + materialAccumulatedDifferences, + globalAccumulatedDifferences + }; + } catch (error) { + if (startTime && initialPosition) { + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const canRecord = runTime > 5 && finalCumulativeDistance > 5; + if (canRecord) { + const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; + const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${error.message}\n\n`; + writeContentToFile(`${CONSTANTS.NO_RECORD_DIR}/${monsterName}.txt`, noRecordContent); + log.info(`${CONSTANTS.LOG_MODULES.RECORD}已将错误路径记录为noRecord模式:${pathName}(实际执行:${runTime.toFixed(1)}秒,${finalCumulativeDistance.toFixed(2)}米)`); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}执行异常但不满足记录条件(运行时间${runTime.toFixed(1)}秒,移动距离${finalCumulativeDistance.toFixed(2)}米),跳过noRecord记录`); + } + } + throw error; } - - await sleep(1); - return { - ...context, - currentMaterialName, - flattenedLowCountMaterials: updatedFlattened, - materialAccumulatedDifferences, - globalAccumulatedDifferences - }; } /** @@ -1400,140 +1472,158 @@ async function processNormalPathEntry(entry, context) { pathRecordCache } = context; - // 用材料名查CD - let refreshCD = null; - for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { - if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; - for (const [cdKey, cdItems] of Object.entries(cdInfo)) { - if (cdItems.includes(resourceName)) { - refreshCD = JSON.parse(cdKey); + let startTime = null; + let initialPosition = null; + let finalPosition = null; + let runTime = 0; + let finalCumulativeDistance = 0; + + try { + let refreshCD = null; + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [cdKey, cdItems] of Object.entries(cdInfo)) { + if (cdItems.includes(resourceName)) { + refreshCD = JSON.parse(cdKey); + break; + } + } + if (refreshCD) break; + } + + if (!refreshCD) { + log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}材料【${resourceName}】未找到CD配置,跳过路径:${pathName}`); + await sleep(1); + return context; + } + + const currentTime = getCurrentTimeInHours(); + const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); + const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir); + const perTime = noRecord ? null : calculatePerTime( + resourceName, + pathName, + recordDir, + noRecordDir, + false, + pathRecordCache, + pathingFilePath + ); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); + + const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir); + log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 预计耗时:${estimatedTime}秒`); + + if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 不符合运行条件`); + await sleep(1); + return context; + } + + const resourceCategoryMap = {}; + for (const [cat, list] of Object.entries(materialCategoryMap)) { + if (list.includes(resourceName)) { + resourceCategoryMap[cat] = [resourceName]; break; } } - if (refreshCD) break; - } - if (!refreshCD) { - log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}材料【${resourceName}】未找到CD配置,跳过路径:${pathName}`); - await sleep(1); - return context; - } + let currentMaterialName = prevMaterialName; + let updatedFlattened = flattenedLowCountMaterials; - // 检查是否可运行 - const currentTime = getCurrentTimeInHours(); - const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); - const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir); - const perTime = noRecord ? null : calculatePerTime( - resourceName, - pathName, - recordDir, - noRecordDir, - false, - pathRecordCache, - pathingFilePath - ); - - log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); - - // 计算并输出预计耗时 - const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir); - log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 预计耗时:${estimatedTime}秒`); - - if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { - log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 不符合运行条件`); - await sleep(1); - return context; - } - - // 构建材料分类映射(用于扫描) - const resourceCategoryMap = {}; - for (const [cat, list] of Object.entries(materialCategoryMap)) { - if (list.includes(resourceName)) { - resourceCategoryMap[cat] = [resourceName]; - break; - } - } - - // 处理运行逻辑 - let currentMaterialName = prevMaterialName; - let updatedFlattened = flattenedLowCountMaterials; - - if (noRecord) { - if (currentMaterialName !== resourceName) { - currentMaterialName = resourceName; - materialAccumulatedDifferences[resourceName] = {}; - log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至材料【${resourceName}】`); - } - - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await runPathAndNotify(pathingFilePath, currentMaterialName); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; - - // 生成内容检测码 - const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; - const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; - writeContentToFile(`${noRecordDir}/${resourceName}.txt`, noRecordContent); - } else { - if (currentMaterialName !== resourceName) { - if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { - const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; - sendNotificationInChunks(prevMsg, notification.Send); + if (noRecord) { + if (currentMaterialName !== resourceName) { + currentMaterialName = resourceName; + materialAccumulatedDifferences[resourceName] = {}; + log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至材料【${resourceName}】`); } - currentMaterialName = resourceName; - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - updatedFlattened = updatedLowCountMaterials - .flat() - .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); - materialAccumulatedDifferences[resourceName] = {}; - } - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await runPathAndNotify(pathingFilePath, currentMaterialName); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + startTime = new Date().toLocaleString(); + initialPosition = genshin.getPositionFromMap(); + await runPathAndNotify(pathingFilePath, currentMaterialName); + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; - // 计算材料变化 - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); - - const materialCountDifferences = {}; - flattenedUpdated.forEach(updated => { - const original = updatedFlattened.find(m => m.name === updated.name); - if (original) { - const diff = parseInt(updated.count) - parseInt(original.count); - if (diff !== 0 || updated.name === resourceName) { - materialCountDifferences[updated.name] = diff; - globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; - materialAccumulatedDifferences[resourceName][updated.name] = (materialAccumulatedDifferences[resourceName][updated.name] || 0) + diff; + const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; + const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; + writeContentToFile(`${noRecordDir}/${resourceName}.txt`, noRecordContent); + } else { + if (currentMaterialName !== resourceName) { + if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { + const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; + sendNotificationInChunks(prevMsg, notification.Send); } + currentMaterialName = resourceName; + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + updatedFlattened = updatedLowCountMaterials + .flat() + .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); + materialAccumulatedDifferences[resourceName] = {}; } - }); - // 更新材料计数缓存 - updatedFlattened = updatedFlattened.map(m => { - const updated = flattenedUpdated.find(u => u.name === m.name); - return updated ? { ...m, count: updated.count } : m; - }); + startTime = new Date().toLocaleString(); + initialPosition = genshin.getPositionFromMap(); + await runPathAndNotify(pathingFilePath, currentMaterialName); + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; - log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}材料路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); - recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); + + const materialCountDifferences = {}; + flattenedUpdated.forEach(updated => { + const original = updatedFlattened.find(m => m.name === updated.name); + if (original) { + const diff = parseInt(updated.count) - parseInt(original.count); + if (diff !== 0 || updated.name === resourceName) { + materialCountDifferences[updated.name] = diff; + globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; + materialAccumulatedDifferences[resourceName][updated.name] = (materialAccumulatedDifferences[resourceName][updated.name] || 0) + diff; + } + } + }); + + updatedFlattened = updatedFlattened.map(m => { + const updated = flattenedUpdated.find(u => u.name === m.name); + return updated ? { ...m, count: updated.count } : m; + }); + + log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}材料路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); + recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + } + + await sleep(1); + return { + ...context, + currentMaterialName, + flattenedLowCountMaterials: updatedFlattened, + materialAccumulatedDifferences, + globalAccumulatedDifferences + }; + } catch (error) { + if (startTime && initialPosition) { + finalPosition = genshin.getPositionFromMap(); + finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const canRecord = runTime > 5 && finalCumulativeDistance > 5; + if (canRecord) { + const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000"; + const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${error.message}\n\n`; + writeContentToFile(`${CONSTANTS.NO_RECORD_DIR}/${resourceName}.txt`, noRecordContent); + log.info(`${CONSTANTS.LOG_MODULES.RECORD}已将错误路径记录为noRecord模式:${pathName}(实际执行:${runTime.toFixed(1)}秒,${finalCumulativeDistance.toFixed(2)}米)`); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}执行异常但不满足记录条件(运行时间${runTime.toFixed(1)}秒,移动距离${finalCumulativeDistance.toFixed(2)}米),跳过noRecord记录`); + } + } + throw error; } - - await sleep(1); - return { - ...context, - currentMaterialName, - flattenedLowCountMaterials: updatedFlattened, - materialAccumulatedDifferences, - globalAccumulatedDifferences - }; } /** @@ -1641,21 +1731,6 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time } catch (singleError) { log.error(`${CONSTANTS.LOG_MODULES.PATH}处理路径出错,已跳过:${singleError.message}`); - // 记录错误路径为noRecord模式 - try { - const pathName = basename(entry.path); - const resourceName = entry.monsterName || entry.resourceName || '未知资源'; - const startTime = new Date().toLocaleString(); - const endTime = new Date().toLocaleString(); - // 生成内容检测码 - const contentCode = entry.path ? generatePathContentCode(entry.path) : "00000000"; - const noRecordContent = `路径名: ${pathName}\n内容检测码: ${contentCode}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: 0秒\n数量变化: ${singleError.message}\n\n`; - writeContentToFile(`${CONSTANTS.NO_RECORD_DIR}/${resourceName}.txt`, noRecordContent); - log.info(`${CONSTANTS.LOG_MODULES.RECORD}已将错误路径记录为noRecord模式:${pathName}`); - } catch (recordError) { - log.error(`${CONSTANTS.LOG_MODULES.RECORD}记录错误路径失败:${recordError.message}`); - } - await sleep(1); if (state.cancelRequested) { log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到终止指令,停止处理`); @@ -1721,11 +1796,13 @@ 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.monsterName},路径:${entry.path}`); - }); - // log.info(`=================================\n`); + if (debugLog) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}\n===== 匹配到的材料路径列表 =====`); + pathEntries.forEach((entry, index) => { + log.info(`${index + 1}. 材料:${entry.resourceName || entry.monsterName},路径:${entry.path}`); + }); + log.info(`=================================\n`); + } } else { log.info(`${CONSTANTS.LOG_MODULES.PATH}未匹配到任何有效的材料路径`); } @@ -1798,6 +1875,9 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames return; } materials.forEach(mat => { + // 添加到pathing怪物材料集合(用于OCR过滤) + ocrContext.pathingMonsterMaterials.add(mat); + const category = matchImageAndGetCategory(mat, imagesDir); if (!category) return; if (!materialCategoryMap[category]) materialCategoryMap[category] = []; @@ -1807,6 +1887,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames } }); }); + if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.MONSTER}pathing文件夹中的怪物材料共${ocrContext.pathingMonsterMaterials.size}种:${Array.from(ocrContext.pathingMonsterMaterials).join('、')}`); } let processedFoodPaths = foodPaths; @@ -1821,23 +1902,23 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames pathingMaterialCounts = allMaterialCounts; // 筛选低数量材料(同时生成超量名单) - log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 基于全量扫描结果筛选有效材料`); + if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 基于全量扫描结果筛选有效材料`); const filteredMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); const validMonsterMaterialNames = filteredMaterials.map(m => m.name); - log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 筛选后有效材料:${validMonsterMaterialNames.join('、')}`); + if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 筛选后有效材料:${validMonsterMaterialNames.join('、')}`); // 普通材料筛选 if (pathingMode.onlyCategory) { return { allPaths: [], pathingMaterialCounts }; } - log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 基于全量扫描结果筛选低数量材料`); + // log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 基于全量扫描结果筛选低数量材料`); const lowCountMaterialsFiltered = filteredMaterials; const flattenedLowCountMaterials = lowCountMaterialsFiltered.flat().sort((a, b) => a.count - b.count); const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name); processedNormalPaths = classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMaterialNames, cdMaterialNames) .filter(entry => normalPaths.some(n => n.path.replace(/\\/g, '/') === entry.path.replace(/\\/g, '/'))); - log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 筛选后保留路径 ${processedNormalPaths.length} 条`); + if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 筛选后保留路径 ${processedNormalPaths.length} 条`); } // 路径优先级规则数组 @@ -1889,7 +1970,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index + 1}] 路径 ${filtered.length} 条`); }); - log.info(`${CONSTANTS.LOG_MODULES.PATH}[最终路径] 共${allPaths.length}条:${allPaths.map(p => basename(p.path))}`); + // log.info(`${CONSTANTS.LOG_MODULES.PATH}[最终路径] 共${allPaths.length}条:${allPaths.map(p => basename(p.path))}`); return { allPaths, pathingMaterialCounts }; } @@ -1989,12 +2070,50 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号,已退出`); return; } - // 过滤超量材料 - allTargetTexts = allTargetTexts.filter(name => !excessMaterialNames.includes(name)); - log.info(`超量名单:${excessMaterialNames.join('、')}`); - log.info(`OCR最终目标文本(已过滤超量):${allTargetTexts.join('、')}`); - await alignAndInteractTarget(allTargetTexts, fDialogueRo, textxRange, texttolerance); + const getFilteredTargetTexts = () => { + let filtered = allTargetTexts.filter(name => !excessMaterialNames.includes(name)); + + if (ocrContext.currentPathType === 'monster') { + // 怪物路径执行时的过滤逻辑: + // 1. 对于怪物材料,只保留: + // - 当前怪物的材料 + // - pathing文件夹中存在且未超量的其他怪物材料 + // 2. 非怪物材料保持不变 + + filtered = filtered.filter(name => { + // 如果不是怪物材料,保留 + if (!materialToMonsters[name]) return true; + + // 如果是怪物材料,检查是否在允许的列表中 + const currentMonsterMaterials = ocrContext.currentTargetMaterials || []; + const pathingMonsterMaterials = Array.from(ocrContext.pathingMonsterMaterials || new Set()); + + // 保留当前怪物的材料或pathing中的怪物材料 + return currentMonsterMaterials.includes(name) || pathingMonsterMaterials.includes(name); + }); + + if (debugLog) { + const currentMonsterMaterials = ocrContext.currentTargetMaterials || []; + const pathingMonsterMaterials = Array.from(ocrContext.pathingMonsterMaterials || new Set()); + const additionalMonsterMaterials = pathingMonsterMaterials.filter(mat => + !currentMonsterMaterials.includes(mat) && !excessMaterialNames.includes(mat) + ); + + log.info(`OCR拾取列表(怪物路径):`); + log.info(` - 当前怪物材料:${currentMonsterMaterials.join('、') || '无'}`); + log.info(` - pathing其他怪物材料(未超量):${additionalMonsterMaterials.join('、') || '无'}`); + log.info(` - 非怪物材料:${filtered.filter(name => !materialToMonsters[name]).join('、') || '无'}`); + } + } + + return filtered; + }; + + log.info(`超量名单:${excessMaterialNames.join('、')}`); + if (debugLog) log.info(`OCR最终目标文本(已过滤超量):${getFilteredTargetTexts().join('、')}`); + + await alignAndInteractTarget(getFilteredTargetTexts, fDialogueRo, textxRange, texttolerance); })(); // 并行任务:路径处理 @@ -2010,23 +2129,27 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 materialList.forEach(name => cdMaterialNames.add(name)); } } - log.info(`${CONSTANTS.LOG_MODULES.CD}CD文件中材料名(已过滤):${Array.from(cdMaterialNames).join(', ')}`); + if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.CD}CD文件中材料名(已过滤):${Array.from(cdMaterialNames).join(', ')}`); // 生成材料分类映射 let materialCategoryMap = {}; // 处理选中的材料分类 - if (selected_materials_array.length > 0) { - // 1. 初始化选中的分类 + if (selected_materials_array.length > 0 && !pathingMode.onlyPathing) { + // 1. 初始化选中的分类(onlyPathing模式除外) selected_materials_array.forEach(selectedCategory => { materialCategoryMap[selectedCategory] = []; }); } else { - log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`); + if (pathingMode.onlyPathing) { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}onlyPathing模式:将自动扫描pathing材料的实际分类`); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`); + } } // 2. 处理路径相关材料(仅includeBoth和onlyPathing模式) - if ((pathingMode.includeBoth || pathingMode.onlyPathing) && Object.keys(materialCategoryMap).length > 0) { + if ((pathingMode.includeBoth || pathingMode.onlyPathing) && (Object.keys(materialCategoryMap).length > 0 || pathingMode.onlyPathing)) { const pathingFilePaths = readAllFilePaths(CONSTANTS.PATHING_DIR, 0, 3, ['.json']); const pathEntries = pathingFilePaths.map(path => { const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames); @@ -2045,7 +2168,10 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 // 构建分类映射 Array.from(allMaterials).forEach(resourceName => { const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR); - if (category && materialCategoryMap[category]) { + if (category) { + if (!materialCategoryMap[category]) { + materialCategoryMap[category] = []; + } if (!materialCategoryMap[category].includes(resourceName)) { materialCategoryMap[category].push(resourceName); } diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json index bbbd8aacc..d23ef80d1 100644 --- a/repo/js/背包材料统计/manifest.json +++ b/repo/js/背包材料统计/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 1, - "name": "背包材料统计及采集管理系统", - "version": "2.60", + "name": "背包统计采集系统", + "version": "2.61", "bgi_version": "0.55", - "description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md", + "description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS,给路径名批量添加检测码,方便识别。", "saved_files": [ "pathing/", "history_record/", diff --git a/repo/js/背包材料统计/settings.json b/repo/js/背包材料统计/settings.json index 168abbe11..6318ac746 100644 --- a/repo/js/背包材料统计/settings.json +++ b/repo/js/背包材料统计/settings.json @@ -1,105 +1,135 @@ -[ - { - "name": "TargetCount", - "type": "input-text", - "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数" - }, - { - "name": "TargetresourceName", - "type": "input-text", - "label": "----------------------------------\n优先级材料,跳过目标数直接运行\n如填入 甜甜花,薄荷,苹果" - }, - { - "name": "TimeCost", - "type": "input-text", - "label": "====================\n时间成本:秒\n一单位材料的平均耗时,默认30" - }, - { - "name": "notify", - "type": "checkbox", - "label": "----------------------------------\n是否发送通知。默认:否\n需在BGI开启JS通知,并设置通知地址" - }, - { - "name": "noRecord", - "type": "checkbox", - "label": "----------------------------------\n取消扫描。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)" - }, - { - "name": "Pathing", - "type": "select", - "label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:仅📁pathing材料", - "options": [ - "1.兼并:📁pathing材料+【材料分类】", - "2.仅📁pathing材料", - "3.仅【材料分类】勾选" - ] - }, - { - "name": "Categories", - "type": "multi-checkbox", - "label": "\n----------------------------------\n【材料分类】", - "options": [ - "矿石、原胚", - "经验书、怪物掉落", - "采集食物", - "一般素材", - "烹饪用食材", - "周本素材", - "木材", - "世界BOSS", - "鱼饵、鱼类", - "宝石", - "天赋素材", - "武器突破" - ], - "default": [ - "一般素材", - "烹饪用食材" - ] - }, - { - "name": "PopupNames", - "type": "input-text", - "label": "数字太小可能无法识别,用?代替\n====================\n弹窗名(默认:全部)" - }, - { - "name": "PopupClickDelay", - "type": "input-text", - "label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)" - }, - { - "name": "CDCategories", - "type": "input-text", - "label": "----------------------------------\n\n采用的CD分类(默认:全部) 举例:采集,怪物,木材" - }, - { - "name": "CurrentTime", - "type": "input-text", - "label": "根据CD分类来加载路径文件,具体看materialsCD目录\n====================\n\n终止时刻(默认:不执行) 例:4:00" - }, - { - "name": "PickCategories", - "type": "input-text", - "label": "====================\n\n采用的识别名单(默认:全部) 举例:交互,采集,宝箱" - }, - { - "name": "ExceedCount", - "type": "input-text", - "label": "举例:交互,采集,宝箱\n----------------------------------\n超量阈值(默认:9000)超量的路径材料将不拾取" - }, - { - "name": "HoldX", - "type": "input-text", - "label": "------------------------\n(0,0)———> X 增加\n |\n |\n V Y 增加\n\n翻页拖动点X坐标:0~1920(默认1050)" - }, - { - "name": "HoldY", - "type": "input-text", - "label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)" - }, - { - "name": "PageScrollDistance", - "type": "input-text", - "label": "------------------------\n拖动距离:(默认711像素点)" - } +[ + { + "name": "TargetCount", + "type": "input-text", + "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数" + }, + { + "name": "TargetresourceName", + "type": "input-text", + "label": "----------------------------------\n优先级材料,跳过目标数直接运行\n如填入 甜甜花,薄荷,苹果" + }, + { + "name": "TimeCost", + "type": "input-text", + "label": "----------------------------------\n时间成本:秒\n一单位材料的平均耗时,默认30" + }, + { + "name": "PickCategories", + "type": "multi-checkbox", + "label": "====================\n\n采用的识别名单(默认:全部)\n不勾选,则默认", + "options": [ + "交互", + "宝箱", + "掉落", + "采集" + ] + }, + { + "name": "ExceedCount", + "type": "input-text", + "label": "----------------------------------\n超量阈值(默认:9000),超量的路径材料将也不拾取\n普通材料超量,或者怪物掉落的三个材料都超量,则跳过其路径" + }, + { + "name": "notify", + "type": "checkbox", + "label": "----------------------------------\n是否发送通知。默认:否\n需在BGI开启JS通知,并设置通知地址" + }, + { + "name": "noRecord", + "type": "checkbox", + "label": "----------------------------------\n取消扫描。默认:否\n勾选将不进行单路径的扫描,但保留运行时间记录\n(推荐路径记录炼成后启用)" + }, + { + "name": "Pathing", + "type": "multi-checkbox", + "label": "====================\n统计选择:📁pathing下的材料,或【扫描额外的分类】的材料。默认:仅📁pathing材料", + "options": [ + "📁pathing材料", + "【扫描额外的分类】" + ], + "default": [ + "📁pathing材料" + ] + }, + { + "name": "Categories", + "type": "multi-checkbox", + "label": "\n----------------------------------\n【扫描的分类】", + "options": [ + "矿石、原胚", + "经验书、怪物掉落", + "采集食物", + "一般素材", + "烹饪用食材", + "周本素材", + "木材", + "世界BOSS", + "鱼饵、鱼类", + "宝石", + "天赋素材", + "武器突破" + ], + "default": [ + "一般素材", + "烹饪用食材" + ] + }, + { + "name": "PopupNames", + "type": "multi-checkbox", + "label": "数字太小可能无法识别,用?代替\n\n====================\n弹窗名(默认:全部),不勾选,则默认。", + "options": [ + "信件", + "冻结", + "复苏", + "对话", + "月卡", + "登录", + "调查", + "过期物品" + ] + }, + { + "name": "PopupClickDelay", + "type": "input-text", + "label": "----------------------------------\n弹窗循环间隔(默认:15 秒)" + }, + { + "name": "CDCategories", + "type": "multi-checkbox", + "label": "====================\n\n根据CD分类,选择加载路径文件(默认:全部)\n不勾选,则默认。", + "options": [ + "怪物", + "掉落", + "木材", + "狗粮", + "采集" + ] + }, + { + "name": "CurrentTime", + "type": "input-text", + "label": "\n\n----------------------------------\n\n终止时刻(默认:不执行) 例:4:00" + }, + { + "name": "HoldX", + "type": "input-text", + "label": "------------------------\n(0,0)———> X 增加\n |\n |\n V Y 增加\n\n翻页拖动点X坐标:0~1920(默认1050)" + }, + { + "name": "HoldY", + "type": "input-text", + "label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)" + }, + { + "name": "PageScrollDistance", + "type": "input-text", + "label": "------------------------\n拖动距离:(默认711像素点)推荐一次划页稍小于4行材料的距离" + }, + { + "name": "debugLog", + "type": "checkbox", + "label": "------------------------\n调试日志。默认:否\n输出详细的调试信息" + } ] \ No newline at end of file diff --git a/repo/js/背包材料统计/targetText/采集.txt b/repo/js/背包材料统计/targetText/采集.txt index d8168c707..84823b45d 100644 --- a/repo/js/背包材料统计/targetText/采集.txt +++ b/repo/js/背包材料统计/targetText/采集.txt @@ -6,5 +6,3 @@ 矿石:萃凝晶,紫晶块,星银矿石,魔晶块,水晶块,白铁块,铁块,虹滴晶, -狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠, -