From 6f63acbc53da653ae12a1940a1592e1866bf4678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E5=90=89=E5=96=B5?= Date: Fri, 27 Feb 2026 13:25:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=87=8C=E6=99=A8CD=E8=AE=A1?= =?UTF-8?q?=E7=AE=97bug=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=B6=85=E9=87=8F?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=BD=9C=E5=9C=A8=E8=AE=B0=E5=BD=95=E6=B1=A1?= =?UTF-8?q?=E6=9F=93bug=20(#2935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo/js/背包材料统计/README.md | 5 +- repo/js/背包材料统计/main.js | 476 ++++++++++++++++------------- repo/js/背包材料统计/manifest.json | 8 +- 3 files changed, 263 insertions(+), 226 deletions(-) diff --git a/repo/js/背包材料统计/README.md b/repo/js/背包材料统计/README.md index defdb80de..86f99f8cb 100644 --- a/repo/js/背包材料统计/README.md +++ b/repo/js/背包材料统计/README.md @@ -1,4 +1,4 @@ -# 背包材料统计 v2.61 +# 背包材料统计 v2.62 作者:吉吉喵 @@ -233,4 +233,5 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各 | v2.58 | 优化背包扫图逻辑 | | v2.59 | 修复自动拾取匹配bug,改为双向匹配 | | v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;材料分类升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错;检测码识别路径 | -| v2.61 | 背包材料识别机制加速;修复手动终止路径noRecord模式的额外条件判断不生效;全局图片缓存;识别名单、CD文件和弹窗升级多选框UI!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! | \ No newline at end of file +| v2.61 | 背包材料识别机制加速;修复手动终止路径noRecord模式的额外条件判断不生效;全局图片缓存;识别名单、CD文件和弹窗升级多选框UI!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! | +| v2.62 | 修复凌晨CD计算bug,修复超量名单潜在记录污染bug | | \ No newline at end of file diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js index dc2f65afd..b2a96e602 100644 --- a/repo/js/背包材料统计/main.js +++ b/repo/js/背包材料统计/main.js @@ -10,7 +10,7 @@ const CONSTANTS = { NO_RECORD_DIR: "pathing_record/noRecord", IMAGES_DIR: "assets/images", MONSTER_MATERIALS_PATH: "assets/Monster-Materials.txt", - + // 解析与处理配置 MAX_PATH_DEPTH: 3, // 路径解析最大深度 NOTIFICATION_CHUNK_SIZE: 500, // 通知拆分长度 @@ -18,7 +18,7 @@ const CONSTANTS = { FOOD_ZERO_EXP_SUFFIX: "_狗粮-0.txt", // 新增:狗粮0 EXP记录后缀 SUMMARY_FILE_NAME: "材料收集汇总.txt", ZERO_COUNT_SUFFIX: "-0.txt", - + // 日志模块标识 LOG_MODULES: { INIT: "[初始化]", @@ -91,22 +91,22 @@ function generatePathContentCode(pathingFilePath) { if (extractedCode) { return extractedCode; } - + // 如果文件名中没有检测码,生成新的 const content = safeReadTextSync(pathingFilePath); if (!content) { log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件为空: ${pathingFilePath}`); return "00000000"; } - + const pathData = JSON.parse(content); const positions = pathData.positions || pathData.actions || []; - + if (!Array.isArray(positions) || positions.length === 0) { log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件无有效位置数据: ${pathingFilePath}`); return "00000000"; } - + return generateContentCode(positions); } catch (error) { log.warn(`${CONSTANTS.LOG_MODULES.PATH}生成路径检测码失败: ${error.message}`); @@ -125,14 +125,14 @@ 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; + if (globalImageCache.has(filePath)) { + return globalImageCache.get(filePath); + } + const mat = file.readImageMatSync(filePath); + if (!mat.empty()) { + globalImageCache.set(filePath, mat); + } + return mat; } // ============================================== @@ -149,34 +149,34 @@ 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; +const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null; // 解析需要处理的CD分类 let allowedCDCategories = []; try { - allowedCDCategories = Array.from(settings.CDCategories || []); + allowedCDCategories = Array.from(settings.CDCategories || []); } catch (e) { - log.error(`${CONSTANTS.LOG_MODULES.INIT}获取CDCategories设置失败: ${e.message}`); + 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(', ')}`); + 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}`); + 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('、')}`); + 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分类过滤,将处理所有分类`); + log.info(`${CONSTANTS.LOG_MODULES.INIT}未配置CD分类过滤,将处理所有分类`); } // ============================================== @@ -212,14 +212,14 @@ function parseMonsterMaterials() { try { const content = safeReadTextSync(CONSTANTS.MONSTER_MATERIALS_PATH); const lines = content.split('\n').map(line => line.trim()).filter(line => line); - + lines.forEach(line => { if (!line.includes(':')) return; const [monsterName, materialsStr] = line.split(':'); const materials = materialsStr.split(/[,,、 \s]+/) .map(mat => mat.trim()) .filter(mat => mat); - + if (monsterName && materials.length > 0) { monsterToMaterials[monsterName] = materials; materials.forEach(mat => { @@ -262,17 +262,17 @@ if (pathingMode.onlyPathing) log.warn(`${CONSTANTS.LOG_MODULES.PATH}已开启【 /** * 初始化并筛选选中的材料分类(适配multi-checkbox的Categories配置) * @returns {string[]} 选中的材料分类列表 - */ -function getSelectedMaterialCategories() { + */ + function getSelectedMaterialCategories() { // 使用Array.from()确保将settings.Categories转换为真正的数组,适配multi-checkbox返回的类数组对象 let selectedCategories = []; - + try { selectedCategories = Array.from(settings.Categories || []); } catch (e) { log.error(`${CONSTANTS.LOG_MODULES.MATERIAL}获取分类设置失败: ${e.message}`); } - + // 兼容旧的checkbox字段名 if (!selectedCategories || selectedCategories.length === 0) { const checkboxToCategory = { @@ -290,25 +290,25 @@ function getSelectedMaterialCategories() { "WeaponAscension": "武器突破", "XP": "祝圣精华" }; - + Object.keys(checkboxToCategory).forEach(checkboxName => { if (settings[checkboxName] === true) { selectedCategories.push(checkboxToCategory[checkboxName]); } }); } - + // 默认分类 if (!selectedCategories || selectedCategories.length === 0) { selectedCategories = ["一般素材", "烹饪用食材"]; } - + // 过滤无效值并映射到实际分类 - return selectedCategories - .filter(cat => typeof cat === 'string' && cat !== "") - .map(name => material_mapping[name] || "锻造素材") - .filter(name => name !== null); -} + return selectedCategories + .filter(cat => typeof cat === 'string' && cat !== "") + .map(name => material_mapping[name] || "锻造素材") + .filter(name => name !== null); + } const selected_materials_array = getSelectedMaterialCategories(); @@ -630,10 +630,10 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath) { const checkDirs = [recordDir, noRecordDir]; let latestEndTime = null; - + // 生成内容检测码 const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : null; - + // 清理路径名中的检测码 const cleanPathName = pathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json'); @@ -645,13 +645,13 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi // 按空行分割成记录块 const recordBlocks = content.split('\n\n').filter(block => block.includes('路径名: ')); - + recordBlocks.forEach(block => { const blockLines = block.split('\n'); let blockPathName = ''; let blockContentCode = '00000000'; let blockEndTime = null; - + blockLines.forEach(line => { if (line.startsWith('路径名: ')) { blockPathName = line.split('路径名: ')[1]; @@ -661,15 +661,15 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi blockEndTime = line.split('结束时间: ')[1]; } }); - + // 清理记录中的路径名检测码 const cleanBlockPathName = blockPathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json'); - + // 匹配条件:路径名相同 或者 内容检测码相同(新逻辑) const isPathMatch = cleanBlockPathName === cleanPathName; const isContentCodeMatch = contentCode && blockContentCode === contentCode; const isMatch = isPathMatch || isContentCodeMatch; - + if (isMatch && blockEndTime) { const endTime = new Date(blockEndTime); if (!latestEndTime || endTime > new Date(latestEndTime)) { @@ -698,10 +698,10 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir, isFood = false, cache = {}, pathingFilePath) { // 生成内容检测码 const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : null; - + // 清理路径名中的检测码 const cleanPathName = pathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json'); - + // 1. 生成唯一缓存键(确保不同路径/不同文件的记录不混淆) const isFoodSuffix = isFood ? CONSTANTS.FOOD_EXP_RECORD_SUFFIX : ".txt"; const recordFile = `${recordDir}/${resourceKey}${isFoodSuffix}`; @@ -738,7 +738,7 @@ function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir, const lines = content.split('\n'); // 先按空行分割成独立的记录块,避免跨记录解析 const recordBlocks = content.split('\n\n').filter(block => block.includes('路径名: ')); - + recordBlocks.forEach(block => { const blockLines = block.split('\n').map(line => line.trim()).filter(line => line); let runTime = 0; @@ -781,7 +781,7 @@ function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir, // 匹配条件:路径名相同 或者 内容检测码相同(新逻辑) const isContentCodeMatch = contentCode && recordContentCode === contentCode; const shouldInclude = (isTargetPath || isContentCodeMatch) && runTime > 0; - + if (shouldInclude) { records.push({ runTime, quantityChange, contentCode: recordContentCode }); } @@ -815,11 +815,11 @@ function estimatePathTotalTime(entry, recordDir, noRecordDir, cache = {}) { // 调用公共函数获取记录(复用缓存) const historicalRecords = getHistoricalPathRecords( - resourceKey, - pathName, - recordDir, - noRecordDir, - isFood, + resourceKey, + pathName, + recordDir, + noRecordDir, + isFood, cache, pathingFilePath ); @@ -854,11 +854,11 @@ function calculatePerTime(resourceName, pathName, recordDir, noRecordDir, isFood const isMonster = monsterToMaterials.hasOwnProperty(resourceName); // 调用公共函数获取记录(复用缓存) const historicalRecords = getHistoricalPathRecords( - resourceName, - pathName, - recordDir, - noRecordDir, - isFood, + resourceName, + pathName, + recordDir, + noRecordDir, + isFood, cache, pathingFilePath ); @@ -873,7 +873,7 @@ function calculatePerTime(resourceName, pathName, recordDir, noRecordDir, isFood if (isMonster) { // 怪物路径:按中级单位计算 const monsterMaterials = monsterToMaterials[resourceName]; - const gradeRatios = [3, 1, 1 / 3]; // 最高级×3,中级×1,最低级×1/3 + const gradeRatios = [3, 1, 1/3]; // 最高级×3,中级×1,最低级×1/3 historicalRecords.forEach(record => { const { runTime, quantityChange } = record; @@ -955,18 +955,28 @@ function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) { log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRunTime.toLocaleString()}`); return canRun; } else if (refreshCD.type === 'specific') { - const specificHour = refreshCD.hour; - const currentDate = new Date(); - const lastDate = new Date(lastEndTimeDate); - const todayRefresh = new Date(currentDate); - todayRefresh.setHours(specificHour, 0, 0, 0); - if (currentDate > todayRefresh && currentDate.getDate() !== lastDate.getDate()) { - return true; - } - const nextRefreshTime = new Date(todayRefresh); - if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1); - log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`); - return false; + const specificHour = refreshCD.hour; + + // 计算上次运行后最近的刷新时间 + const lastRefreshAfterRun = new Date(lastEndTimeDate); + lastRefreshAfterRun.setHours(specificHour, 0, 0, 0); + if (lastRefreshAfterRun <= lastEndTimeDate) { + lastRefreshAfterRun.setDate(lastRefreshAfterRun.getDate() + 1); + } + + // 如果当前时间已经过了最近的刷新时间,允许运行 + if (currentDate >= lastRefreshAfterRun) { + log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},最近刷新时间:${lastRefreshAfterRun.toLocaleString()},允许运行`); + return true; + } + + // 计算下次刷新时间 + const todayRefresh = new Date(currentDate); + todayRefresh.setHours(specificHour, 0, 0, 0); + const nextRefreshTime = new Date(todayRefresh); + if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1); + log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`); + return false; } else if (refreshCD.type === 'instant') { return true; } @@ -998,7 +1008,7 @@ const imageMapCache = new Map(); // 保持固定,不动态刷新 const createImageCategoryMap = (imagesDir) => { const map = {}; const imageFiles = readAllFilePaths(imagesDir, 0, 1, ['.png']); - + for (const imagePath of imageFiles) { const pathParts = imagePath.split(/[\\/]/); if (pathParts.length < 3) continue; @@ -1007,12 +1017,12 @@ const createImageCategoryMap = (imagesDir) => { .replace(/\.png$/i, '') .trim() .toLowerCase(); - + if (!(imageName in map)) { map[imageName] = pathParts[2]; } } - + return map; }; @@ -1026,7 +1036,7 @@ const loggedResources = new Set(); */ function matchImageAndGetCategory(resourceName, imagesDir) { const processedName = (MATERIAL_ALIAS[resourceName] || resourceName).toLowerCase(); - + if (!imageMapCache.has(imagesDir)) { log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}初始化图像分类缓存:${imagesDir}`); imageMapCache.set(imagesDir, createImageCategoryMap(imagesDir)); @@ -1042,7 +1052,7 @@ function matchImageAndGetCategory(resourceName, imagesDir) { if (!loggedResources.has(processedName)) { loggedResources.add(processedName); } - + return result; } @@ -1179,22 +1189,22 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir, const currentTime = getCurrentTimeInHours(); const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); const perTime = noRecord ? null : calculatePerTime( - resourceName, - pathName, - recordDir, - noRecordDir, + 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 + const canRun = canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) + && isPathValid && (noRecord || perTime === null || perTime <= timeCost); if (!canRun) { @@ -1252,7 +1262,7 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir, 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"; @@ -1276,9 +1286,9 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir, async function processMonsterPathEntry(entry, context) { const { path: pathingFilePath, monsterName } = entry; const pathName = basename(pathingFilePath); - const { + const { CDCategories, timeCost, recordDir, noRecordDir, imagesDir, - materialCategoryMap, flattenedLowCountMaterials, + materialCategoryMap, flattenedLowCountMaterials, currentMaterialName: prevMaterialName, materialAccumulatedDifferences, globalAccumulatedDifferences, pathRecordCache @@ -1321,17 +1331,17 @@ async function processMonsterPathEntry(entry, context) { const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir, pathingFilePath); const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir); const perTime = noRecord ? null : calculatePerTime( - monsterName, - pathName, - recordDir, - noRecordDir, + 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}秒`); @@ -1423,7 +1433,17 @@ async function processMonsterPathEntry(entry, context) { }); log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}怪物路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); - recordRunTime(monsterName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + // 检查怪物对应的材料是否有超量,如果有,记录到noRecord目录 + let isExcess = false; + const monsterMaterials = monsterToMaterials[monsterName] || []; + for (const material of monsterMaterials) { + if (excessMaterialNames.includes(material)) { + isExcess = true; + break; + } + } + const targetRecordDir = isExcess ? noRecordDir : recordDir; + recordRunTime(monsterName, pathName, startTime, endTime, runTime, targetRecordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); } await sleep(1); @@ -1440,7 +1460,7 @@ async function processMonsterPathEntry(entry, context) { 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"; @@ -1464,9 +1484,9 @@ async function processMonsterPathEntry(entry, context) { async function processNormalPathEntry(entry, context) { const { path: pathingFilePath, resourceName } = entry; const pathName = basename(pathingFilePath); - const { + const { CDCategories, timeCost, recordDir, noRecordDir, - materialCategoryMap, flattenedLowCountMaterials, + materialCategoryMap, flattenedLowCountMaterials, currentMaterialName: prevMaterialName, materialAccumulatedDifferences, globalAccumulatedDifferences, pathRecordCache @@ -1501,17 +1521,17 @@ async function processNormalPathEntry(entry, context) { const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath); const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir); const perTime = noRecord ? null : calculatePerTime( - resourceName, - pathName, - recordDir, - noRecordDir, + 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}秒`); @@ -1594,7 +1614,23 @@ async function processNormalPathEntry(entry, context) { }); log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}材料路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); - recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); + // 检查材料是否在超量名单中,如果是,记录到noRecord目录 + let isExcess = false; + // 检查当前材料是否超量 + if (excessMaterialNames.includes(resourceName)) { + isExcess = true; + } else if (monsterToMaterials.hasOwnProperty(resourceName)) { + // 对于怪物路径,检查其对应的材料是否有超量 + const monsterMaterials = monsterToMaterials[resourceName]; + for (const material of monsterMaterials) { + if (excessMaterialNames.includes(material)) { + isExcess = true; + break; + } + } + } + const targetRecordDir = isExcess ? noRecordDir : recordDir; + recordRunTime(resourceName, pathName, startTime, endTime, runTime, targetRecordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath); } await sleep(1); @@ -1611,7 +1647,7 @@ async function processNormalPathEntry(entry, context) { 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"; @@ -1647,7 +1683,7 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time const globalAccumulatedDifferences = {}; const materialAccumulatedDifferences = {}; // 单路径处理周期内的记录缓存 - const pathRecordCache = {}; + const pathRecordCache = {}; let context = { CDCategories, timeCost, recordDir, noRecordDir, imagesDir, materialCategoryMap, flattenedLowCountMaterials, @@ -1676,9 +1712,9 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time } const pathTotalTimeSec = estimatePathTotalTime( - entry, - recordDir, - noRecordDir, + entry, + recordDir, + noRecordDir, pathRecordCache ); const pathTotalTimeMin = pathTotalTimeSec / 60; @@ -1706,15 +1742,15 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time if (resourceName && isFoodResource(resourceName)) { // 狗粮路径:传递完整校验参数 const result = await processFoodPathEntry( - entry, + entry, { foodExpAccumulator, currentMaterialName: context.currentMaterialName - }, - recordDir, - noRecordDir, - CDCategories, - timeCost, + }, + recordDir, + noRecordDir, + CDCategories, + timeCost, context.pathRecordCache ); foodExpAccumulator = result.foodExpAccumulator; @@ -1730,7 +1766,7 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time } } catch (singleError) { log.error(`${CONSTANTS.LOG_MODULES.PATH}处理路径出错,已跳过:${singleError.message}`); - + await sleep(1); if (state.cancelRequested) { log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到终止指令,停止处理`); @@ -1750,8 +1786,8 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time } } - return { - currentMaterialName: context.currentMaterialName, + return { + currentMaterialName: context.currentMaterialName, flattenedLowCountMaterials: context.flattenedLowCountMaterials, globalAccumulatedDifferences: context.globalAccumulatedDifferences, foodExpAccumulator @@ -1871,13 +1907,13 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames monsterPaths.forEach((entry, index) => { const materials = monsterToMaterials[entry.monsterName] || []; if (materials.length === 0) { - log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物路径${index + 1}] 怪物【${entry.monsterName}】无对应材料映射`); + log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物路径${index+1}] 怪物【${entry.monsterName}】无对应材料映射`); return; } materials.forEach(mat => { // 添加到pathing怪物材料集合(用于OCR过滤) ocrContext.pathingMonsterMaterials.add(mat); - + const category = matchImageAndGetCategory(mat, imagesDir); if (!category) return; if (!materialCategoryMap[category]) materialCategoryMap[category] = []; @@ -1924,41 +1960,41 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames // 路径优先级规则数组 const PATH_PRIORITIES = [ // 1. 目标狗粮 - { - source: processedFoodPaths, - filter: e => targetResourceNames.includes(e.resourceName) + { + source: processedFoodPaths, + filter: e => targetResourceNames.includes(e.resourceName) }, // 2. 目标怪物(掉落材料含目标) - { - source: processedMonsterPaths, + { + source: processedMonsterPaths, filter: e => { const materials = monsterToMaterials[e.monsterName] || []; return materials.some(mat => targetResourceNames.includes(mat)); - } + } }, // 3. 目标普通材料 - { - source: processedNormalPaths, - filter: e => targetResourceNames.includes(e.resourceName) + { + source: processedNormalPaths, + filter: e => targetResourceNames.includes(e.resourceName) }, // 4. 剩余狗粮 - { - source: processedFoodPaths, - filter: e => !targetResourceNames.includes(e.resourceName) + { + source: processedFoodPaths, + filter: e => !targetResourceNames.includes(e.resourceName) }, // 5. 剩余怪物(掉落材料未超量且低数量) - { - source: processedMonsterPaths, + { + source: processedMonsterPaths, filter: e => { const materials = monsterToMaterials[e.monsterName] || []; - return !materials.some(mat => targetResourceNames.includes(mat)) && - materials.some(mat => !excessMaterialNames.includes(mat)); - } + return !materials.some(mat => targetResourceNames.includes(mat)) && + materials.some(mat => !excessMaterialNames.includes(mat)); + } }, // 6. 剩余普通材料 - { - source: processedNormalPaths, - filter: e => !targetResourceNames.includes(e.resourceName) + { + source: processedNormalPaths, + filter: e => !targetResourceNames.includes(e.resourceName) } ]; @@ -1967,7 +2003,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames PATH_PRIORITIES.forEach(({ source, filter }, index) => { const filtered = source.filter(filter); allPaths.push(...filtered); - log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index + 1}] 路径 ${filtered.length} 条`); + log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index+1}] 路径 ${filtered.length} 条`); }); // log.info(`${CONSTANTS.LOG_MODULES.PATH}[最终路径] 共${allPaths.length}条:${allPaths.map(p => basename(p.path))}`); @@ -1994,13 +2030,13 @@ function sendNotificationInChunks(msg, sendFn) { const totalChunks = Math.ceil(msg.length / chunkSize); log.info(`${CONSTANTS.LOG_MODULES.MAIN}通知消息过长(${msg.length}字符),拆分为${totalChunks}段发送`); - + let start = 0; for (let i = 0; i < totalChunks; i++) { const end = Math.min(start + chunkSize, msg.length); - const chunkMsg = `【通知${i + 1}/${totalChunks}】\n${msg.substring(start, end)}`; + const chunkMsg = `【通知${i+1}/${totalChunks}】\n${msg.substring(start, end)}`; sendFn(chunkMsg); - log.info(`${CONSTANTS.LOG_MODULES.MAIN}已发送第${i + 1}段通知(${chunkMsg.length}字符)`); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}已发送第${i+1}段通知(${chunkMsg.length}字符)`); start = end; } } @@ -2062,7 +2098,7 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 // 等待超量名单生成 let waitTimes = 0; - while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) { + while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) { await sleep(1000); waitTimes++; } @@ -2070,43 +2106,43 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号,已退出`); return; } - + 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 => + 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; }; @@ -2132,78 +2168,78 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.CD}CD文件中材料名(已过滤):${Array.from(cdMaterialNames).join(', ')}`); // 生成材料分类映射 - let materialCategoryMap = {}; - - // 处理选中的材料分类 - if (selected_materials_array.length > 0 && !pathingMode.onlyPathing) { - // 1. 初始化选中的分类(onlyPathing模式除外) - selected_materials_array.forEach(selectedCategory => { - materialCategoryMap[selectedCategory] = []; - }); + let materialCategoryMap = {}; + + // 处理选中的材料分类 + if (selected_materials_array.length > 0 && !pathingMode.onlyPathing) { + // 1. 初始化选中的分类(onlyPathing模式除外) + selected_materials_array.forEach(selectedCategory => { + materialCategoryMap[selectedCategory] = []; + }); + } else { + if (pathingMode.onlyPathing) { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}onlyPathing模式:将自动扫描pathing材料的实际分类`); } else { - if (pathingMode.onlyPathing) { - log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}onlyPathing模式:将自动扫描pathing材料的实际分类`); - } else { - log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`); - } + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`); } + } + + // 2. 处理路径相关材料(仅includeBoth和onlyPathing模式) + 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); + return { materialName, monsterName }; + }); - // 2. 处理路径相关材料(仅includeBoth和onlyPathing模式) - 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); - return { materialName, monsterName }; - }); + // 收集所有材料(含怪物掉落) + const allMaterials = new Set(); + pathEntries.forEach(({ materialName, monsterName }) => { + if (materialName) allMaterials.add(materialName); + if (monsterName) { + (monsterToMaterials[monsterName] || []).forEach(mat => allMaterials.add(mat)); + } + }); - // 收集所有材料(含怪物掉落) - const allMaterials = new Set(); - pathEntries.forEach(({ materialName, monsterName }) => { - if (materialName) allMaterials.add(materialName); - if (monsterName) { - (monsterToMaterials[monsterName] || []).forEach(mat => allMaterials.add(mat)); + // 构建分类映射 + Array.from(allMaterials).forEach(resourceName => { + const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR); + if (category) { + if (!materialCategoryMap[category]) { + materialCategoryMap[category] = []; } - }); - - // 构建分类映射 - Array.from(allMaterials).forEach(resourceName => { - const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR); - if (category) { - if (!materialCategoryMap[category]) { - materialCategoryMap[category] = []; - } - if (!materialCategoryMap[category].includes(resourceName)) { - materialCategoryMap[category].push(resourceName); - } + if (!materialCategoryMap[category].includes(resourceName)) { + materialCategoryMap[category].push(resourceName); + } + } + }); + } + + // 3. 在onlyPathing或onlyCategory模式下,保留所有选中的分类 + if (pathingMode.onlyPathing || pathingMode.onlyCategory) { + // 对于onlyCategory模式,保留所有选中的分类,即使是空数组 + // 对于onlyPathing模式,删除空数组 + if (pathingMode.onlyPathing) { + Object.keys(materialCategoryMap).forEach(category => { + if (materialCategoryMap[category].length === 0) { + delete materialCategoryMap[category]; } }); } - - // 3. 在onlyPathing或onlyCategory模式下,保留所有选中的分类 - if (pathingMode.onlyPathing || pathingMode.onlyCategory) { - // 对于onlyCategory模式,保留所有选中的分类,即使是空数组 - // 对于onlyPathing模式,删除空数组 - if (pathingMode.onlyPathing) { - Object.keys(materialCategoryMap).forEach(category => { - if (materialCategoryMap[category].length === 0) { - delete materialCategoryMap[category]; - } - }); - } - } - - // 4. 在onlyCategory模式下,确保materialCategoryMap只包含选中的分类 - if (pathingMode.onlyCategory) { - const selectedCategoriesSet = new Set(selected_materials_array); - const filteredMap = {}; - - // 只保留选中的分类,即使是空数组 - selected_materials_array.forEach(category => { - filteredMap[category] = materialCategoryMap[category] || []; - }); - - materialCategoryMap = filteredMap; - } + } + + // 4. 在onlyCategory模式下,确保materialCategoryMap只包含选中的分类 + if (pathingMode.onlyCategory) { + const selectedCategoriesSet = new Set(selected_materials_array); + const filteredMap = {}; + + // 只保留选中的分类,即使是空数组 + selected_materials_array.forEach(category => { + filteredMap[category] = materialCategoryMap[category] || []; + }); + + materialCategoryMap = filteredMap; + } // 生成路径数组 const { allPaths, pathingMaterialCounts } = await generateAllPaths( diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json index d23ef80d1..c1d62489a 100644 --- a/repo/js/背包材料统计/manifest.json +++ b/repo/js/背包材料统计/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 1, "name": "背包统计采集系统", - "version": "2.61", + "version": "2.62", "bgi_version": "0.55", - "description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS,给路径名批量添加检测码,方便识别。", +"description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS,给路径名批量添加检测码,方便识别。", "saved_files": [ "pathing/", "history_record/", @@ -13,8 +13,8 @@ ], "authors": [ { - "name": "吉吉喵", - "links": "https://github.com/JJMdzh" + "name": "吉吉喵", + "links": "https://github.com/JJMdzh" } ], "settings_ui": "settings.json",