From 431927c46fdcc8ff2e37d5b2e646f25c1d1c6685 Mon Sep 17 00:00:00 2001 From: JJMdzh Date: Sat, 24 May 2025 21:13:40 +0800 Subject: [PATCH] =?UTF-8?q?js=EF=BC=8C=E8=83=8C=E5=8C=85=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?++=20v2.26=20=E6=9B=B4=E5=BF=AB=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E6=9D=90=E6=96=99=E6=97=B6=E9=97=B4=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=AD=89bug=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=9D=90=E6=96=99=E6=97=B6=E9=97=B4=E6=88=90=E6=9C=AC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=20(#915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * js,背包统计++ v2.26 更快,修复读取材料时间错误等bug,新增路径材料时间成本计算 + v2.26 修复读取材料时间错误等bug,新增路径材料时间成本计算; 因食物部分图片未补足,为适配快速滑页,苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪,这八个食物必须有,且在第一行。不然这几个食物会无法识别。 * Switch to parseFloat to preserve run-time precision in the time-cost calculation. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: 秋云 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- repo/js/背包材料统计/README.md | 9 +- repo/js/背包材料统计/main.js | 191 ++++++++++++++++++++++++----- repo/js/背包材料统计/manifest.json | 4 +- repo/js/背包材料统计/settings.json | 43 ++++--- 4 files changed, 191 insertions(+), 56 deletions(-) diff --git a/repo/js/背包材料统计/README.md b/repo/js/背包材料统计/README.md index 4b73e21e..e2b52458 100644 --- a/repo/js/背包材料统计/README.md +++ b/repo/js/背包材料统计/README.md @@ -51,12 +51,16 @@ 1. 将脚本添加至调度器。 2. 右键点击脚本以修改JS自定义配置。 3. 执行路径功能,需要📁pathing有路径文件夹 + +## 注意 +因食物部分图片未补足,为适配快速滑页,苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪,这八个食物必须有,且在第一行。不然这几个食物会无法识别。 + ## 后言 本脚本目前处于测试阶段,欢迎反馈问题至 QQ频道号: bettergiv1。 ## 更新日志 + v0.1 OCR名单 输出图片名和材料名 -+ v1.0图包(素材) ++ v1.0 图包(素材) + v1.1 图包(素材+养成道具) + v1.2 识图分类 + v1.3 加速寻找(前位材料识别) @@ -70,4 +74,5 @@ + v2.22 精简log + v2.23 优化部分函数 + v2.24 修复不能空路径使用背包统计功能等bug -+ v2.25 当前、后位材料识别(加速扫描), 新增只扫描路径材料名选项(内存占用更小) \ No newline at end of file ++ v2.25 当前、后位材料识别(加速扫描),新增只扫描路径材料名选项(内存占用更小) ++ v2.26 修复读取材料时间错误等bug,新增路径材料时间成本计算 \ No newline at end of file diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js index c38ec608..c2bbf02a 100644 --- a/repo/js/背包材料统计/main.js +++ b/repo/js/背包材料统计/main.js @@ -1,7 +1,8 @@ -const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.targetCount) || 5000))); // 设定的目标数量 +const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量 const OCRdelay = Math.min(50, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长 -const Imagedelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 +const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 +const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); // 耗时和材料数量的比值,即一个材料多少秒 // 定义映射表"unselected": "反选材料分类", const material_mapping = { "General": "一般素材", @@ -19,19 +20,28 @@ const material_mapping = { } const isOnlyPathing = settings.onlyPathing === "否" ? false : true; +// 初始化 settings,将 material_mapping 中的所有键设置为 false +const initialSettings = Object.keys(material_mapping).reduce((acc, key) => { + acc[key] = false; + return acc; +}, {}); + +// 合并初始设置和实际的 settings,实际的 settings 会覆盖初始设置 +const finalSettings = { ...initialSettings, ...settings }; + // 检查是否启用反选功能 -const isUnselected = settings.unselected === true; +const isUnselected = finalSettings.unselected === true; // 根据反选功能生成选中的材料分类数组 -const selected_materials_array = Object.keys(settings) +const selected_materials_array = Object.keys(finalSettings) .filter(key => key !== "unselected") // 排除 "unselected" 键 .filter(key => { - // 确保 settings[key] 是布尔值 - if (typeof settings[key] !== 'boolean') { - console.warn(`非布尔值的键: ${key}, 值: ${settings[key]}`); + // 确保 finalSettings[key] 是布尔值 + if (typeof finalSettings[key] !== 'boolean') { + console.warn(`非布尔值的键: ${key}, 值: ${finalSettings[key]}`); return false; } - return isUnselected ? !settings[key] : settings[key]; + return isUnselected ? !finalSettings[key] : finalSettings[key]; }) .map(name => { // 确保 material_mapping 中存在对应的键 @@ -202,7 +212,7 @@ function filterMaterialsByPriority(materialsCategory) { } // 扫描材料 -async function scanMaterials(materialsCategory, materialCategoryMap, targetResourceName = false, targetMaterialNames = []) { +async function scanMaterials(materialsCategory, materialCategoryMap) { // 获取当前+后位材料名单 const priorityMaterialNames = []; const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory); @@ -339,7 +349,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap, targetResou recognitionObject.threshold = 0.85; const result = captureGameRegion().find(recognitionObject); - await sleep(Imagedelay); + await sleep(imageDelay); if (result.isExist() && result.x !== 0 && result.y !== 0) { recognizedMaterials.add(name); @@ -817,7 +827,7 @@ function extractResourceNameFromPath(filePath) { // 从 materials 文件夹中读取分类信息 function readMaterialCategories(materialDir) { // log.info(`开始读取材料分类信息:${materialDir}`); - const materialFilePaths = readAllFilePaths(materialDir, 0, 1); + const materialFilePaths = readAllFilePaths(materialDir, 0, 1, ['.txt']); const materialCategories = {}; for (const filePath of materialFilePaths) { @@ -839,9 +849,9 @@ function getCurrentTimeInHours() { return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600; } // 记录运行时间到材料对应的文件中 -function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir) { +function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}) { const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名 - const content = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n\n`; + const content = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; try { // 只有当运行时间大于或等于3秒时,才记录运行时间 @@ -878,7 +888,9 @@ function getLastRunEndTime(resourceName, pathName, recordDir) { try { const content = file.readTextSync(recordPath); // 同步读取记录文件 const lines = content.split('\n'); - for (let i = lines.length - 1; i >= 0; i--) { + + // 从文件内容的开头开始查找 + for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('路径名: ')) { const currentPathName = lines[i].split('路径名: ')[1]; if (currentPathName === pathName) { @@ -894,6 +906,69 @@ function getLastRunEndTime(resourceName, pathName, recordDir) { } return null; // 如果未找到记录文件或结束时间,返回 null } + +// 计算时间成本 +function calculatePerTime(resourceName, pathName, recordDir) { + const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名 + try { + const content = file.readTextSync(recordPath); // 同步读取记录文件 + const lines = content.split('\n'); + + const completeRecords = []; // 用于存储完整的记录 + + // 从文件内容的开头开始查找 + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('路径名: ')) { + const currentPathName = lines[i].split('路径名: ')[1]; + if (currentPathName === pathName) { + const runTimeLine = lines[i + 3]; // 假设运行时间在路径名后的第四行 + const quantityChangeLine = lines[i + 4]; // 假设数量变化在路径名后的第五行 + + if (runTimeLine.startsWith('运行时间: ') && quantityChangeLine.startsWith('数量变化: ')) { + const runTime = parseFloat(runTimeLine.split('运行时间: ')[1].split('秒')[0]); + const quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]); + + // 检查数量变化是否有效 + if (quantityChange[resourceName] !== undefined && quantityChange[resourceName] !== 0) { + const perTime = runTime / quantityChange[resourceName]; + completeRecords.push(perTime); + } + } + } + } + } + + // 如果完整记录少于3条,返回 null + if (completeRecords.length < 3) { + log.warn(`完整记录不足3条,无法计算有效的时间成本: ${recordPath}`); + return null; + } + + // 只考虑最近的5条记录 + const recentRecords = completeRecords.slice(-5); + + // 计算平均值和标准差 + const mean = recentRecords.reduce((acc, val) => acc + val, 0) / recentRecords.length; + const stdDev = Math.sqrt(recentRecords.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / recentRecords.length); + + // 排除差异过大的数据 + const filteredRecords = recentRecords.filter(record => Math.abs(record - mean) <= 2 * stdDev); + + // 如果过滤后没有剩余数据,返回 null + if (filteredRecords.length === 0) { + log.warn(`所有记录数据差异过大,无法计算有效的时间成本: ${recordPath}`); + return null; + } + + // 计算平均时间成本 + const averagePerTime = filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length; + return averagePerTime; + } catch (error) { + log.warn(`缺失耗时或者数量变化,无法计算时间成本: ${recordPath}`); + } + return null; // 如果未找到记录文件或效率数据,返回 null +} + // 判断是否可以运行脚本 function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) { if (!lastEndTime) { @@ -963,7 +1038,7 @@ const imageMapCache = new Map(); const createImageCategoryMap = (imagesDir) => { const map = {}; - const imageFiles = readAllFilePaths(imagesDir, 0, 1); + const imageFiles = readAllFilePaths(imagesDir, 0, 1, ['.png']); for (const imagePath of imageFiles) { const pathParts = imagePath.split(/[\\/]/); @@ -1014,7 +1089,7 @@ function matchImageAndGetCategory(resourceName, imagesDir) { const imagesDir = "assets\\images"; // 存储图片的文件夹 // 从设置中获取目标材料名称 - const targetResourceNamesStr = settings.targetresourceName || ""; + const targetResourceNamesStr = settings.TargetresourceName || ""; // 使用正则表达式分割字符串,支持多种分隔符(如逗号、分号、空格等) const targetResourceNames = targetResourceNamesStr @@ -1030,7 +1105,7 @@ function matchImageAndGetCategory(resourceName, imagesDir) { const materialCategories = readMaterialCategories(materialDir); // 递归读取路径信息文件夹 - const pathingFilePaths = readAllFilePaths(pathingDir, 0, 1); + const pathingFilePaths = readAllFilePaths(pathingDir, 0, 1, ['.json']); // 将路径和资源名绑定,避免重复提取 @@ -1065,7 +1140,6 @@ function matchImageAndGetCategory(resourceName, imagesDir) { } }); // 如果 isOnlyPathing 为 true,移除 materialCategoryMap 中的空数组 - // 如果 isOnlyPathing 为 true,移除 materialCategoryMap 中的空数组 if (isOnlyPathing) { Object.keys(materialCategoryMap).forEach(category => { if (materialCategoryMap[category].length === 0) { @@ -1127,7 +1201,7 @@ normalPaths.sort((a, b) => { const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径 // log.info(`处理路径文件:${pathingFilePath},材料名:${resourceName},材料路径:${pathName}`); - // 查找材料对应的分类 + // 查找材料对应的CD分类 let categoryFound = false; for (const [category, materials] of Object.entries(materialCategories)) { for (const [refreshCDKey, materialList] of Object.entries(materials)) { @@ -1139,31 +1213,82 @@ normalPaths.sort((a, b) => { // 读取上次运行的结束时间 const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir); + // 计算效率 + const perTime = calculatePerTime(resourceName, pathName, recordDir); + + log.info(`路径文件:${pathName}单个材料耗时:${perTime}秒`); // 判断是否可以运行脚本 - if (canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName)) { + if (canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && (perTime === null || perTime <= timeCost)) { log.info(`可调用路径文件:${pathName}`); - // 记录开始时间 - const startTime = new Date().toLocaleString(); + // 记录开始时间 + const startTime = new Date().toLocaleString(); - // 调用路径文件 - await pathingScript.runFile(pathingFilePath); - await sleep(1000); + // 调用路径文件 + await pathingScript.runFile(pathingFilePath); + await sleep(1000); - // 记录结束时间 - const endTime = new Date().toLocaleString(); + // 根据 materialCategoryMap 构建 resourceCategoryMap + const resourceCategoryMap = {}; + for (const [materialCategory, materialList] of Object.entries(materialCategoryMap)) { + if (materialList.includes(resourceName)) { + resourceCategoryMap[materialCategory] = [resourceName]; + break; + } + } - // 计算运行时间 - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; // 秒 + // 输出 resourceCategoryMap 以供调试 + log.info(`resourceCategoryMap: ${JSON.stringify(resourceCategoryMap, null, 2)}`); - // 记录运行时间到材料对应的文件中 - recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir); + // 调用背包材料统计(获取调用路径文件后的材料数量) + const updatedMaterialCounts = await MaterialPath(resourceCategoryMap); - categoryFound = true; + // 展平数组并按数量从小到大排序 + const flattenedUpdatedMaterialCounts = updatedMaterialCounts + .flat() + .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); + + // 提取更新后的低数量材料的名称 + const updatedLowCountMaterialNames = flattenedUpdatedMaterialCounts.map(material => material.name); + + // 创建一个映射,用于存储更新前后的数量差值 + const materialCountDifferences = {}; + + // 遍历更新后的材料数量,计算差值 + flattenedUpdatedMaterialCounts.forEach(updatedMaterial => { + const originalMaterial = flattenedLowCountMaterials.find(material => material.name === updatedMaterial.name); + if (originalMaterial) { + const originalCount = parseInt(originalMaterial.count, 10); + const updatedCount = parseInt(updatedMaterial.count, 10); + const difference = updatedCount - originalCount; + if (difference !== 0) { // 只记录数量变化不为0的材料 + materialCountDifferences[updatedMaterial.name] = difference; + } + } + }); + + // 打印数量差值 + log.info(`数量变化: ${JSON.stringify(materialCountDifferences, null, 2)}`); + + // 记录结束时间 + const endTime = new Date().toLocaleString(); + + // 计算运行时间 + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; // 秒 + + // 记录运行时间到材料对应的文件中 + recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences); + log.info(`当前材料名: ${JSON.stringify(resourceName, null, 2)}`); + + categoryFound = true; break; } else { - log.info(`路径文件 ${pathName} 还未到刷新时间`); + if (perTime !== null && perTime > timeCost) { + log.info(`路径文件 ${pathName} 的单个材料耗时大于 ${timeCost} ,不执行`); + } else { + log.info(`路径文件 ${pathName} 未能执行!`); + } } } } diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json index 2a7d57c4..bc927b5b 100644 --- a/repo/js/背包材料统计/manifest.json +++ b/repo/js/背包材料统计/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 1, "name": "背包材料统计", - "version": "2.25", + "version": "2.26", "bgi_version": "0.44.8", - "description": "默认四行为一页;模板匹配材料,OCR识别数量。\n数字太小可能无法识别,用?代替。\n目前支持采集路线。\n材料种类数量或导入js本地\n图包文件夹images放入assets下\nv2.24修复不能空路径使用背包统计功能等bug", + "description": "默认四行为一页;模板匹配材料,OCR识别数量。\n数字太小可能无法识别,用?代替。\n目前支持采集材料的数量统计+路径CD管理。", "authors": [ { "name": "吉吉喵" diff --git a/repo/js/背包材料统计/settings.json b/repo/js/背包材料统计/settings.json index 6d2246b4..ed989d47 100644 --- a/repo/js/背包材料统计/settings.json +++ b/repo/js/背包材料统计/settings.json @@ -1,18 +1,23 @@ [ { - "name": "targetresourceName", + "name": "TargetresourceName", "type": "input-text", - "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n-----------------\n优先级材料名,不受目标数量影响。" + "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数" }, { - "name": "targetCount", + "name": "TargetCount", "type": "input-text", - "label": "如填入 甜甜花,薄荷,苹果\n-----------------\n\n目标数量,默认5000" + "label": "----------------------------------\n优先级材料,跳过目标数直接运行\n如填入 甜甜花,薄荷,苹果" + }, + { + "name": "TimeCost", + "type": "input-text", + "label": "====================\n时间成本:秒\n1数量的材料平均耗时,默认最多30" }, { "name": "onlyPathing", "type": "select", - "label": "上2项可不填\n=============\n只扫描pathing文件夹中材料名\n的数量,默认:是", + "label": "====================\n只扫描📁pathing下的材料\n无视【材料分类】勾选。默认:是", "options": [ "是", "否", @@ -21,71 +26,71 @@ { "name": "unselected", "type": "checkbox", - "label": "=============\n反选材料分类" + "label": "====================\n反选下述【材料分类】" }, { "name": "Smithing", "type": "checkbox", - "label": "-----------------\n锻造素材" + "label": "\n----------------------------------\n【锻造素材】" }, { "name": "Drops", "type": "checkbox", - "label": "如:矿石、原胚\n-----------------\n怪物掉落素材" + "label": "如:矿石、原胚\n----------------------------------\n【怪物掉落素材】" }, { "name": "ForagedFood", "type": "checkbox", - "label": "-----------------\n采集食物,食用回血" + "label": "----------------------------------\n【采集食物】,食用回血" }, { "name": "General", "type": "checkbox", - "label": "如:苹果、日落果、泡泡桔\n-----------------\n一般素材" + "label": "如:苹果、日落果、泡泡桔\n----------------------------------\n【一般素材】" }, { "name": "CookingIngs", "type": "checkbox", - "label": "如:特产、非食用素材\n-----------------\n烹饪用食材" + "label": "如:特产、非食用素材\n----------------------------------\n\n【烹饪用食材】" }, { "name": "Weekly", "type": "checkbox", - "label": "-----------------\n\n周本素材" + "label": "----------------------------------\n\n【周本素材】" }, { "name": "Wood", "type": "checkbox", - "label": "-----------------\n\n木材" + "label": "----------------------------------\n\n【木材】" }, { "name": "CharAscension", "type": "checkbox", - "label": "-----------------\n\n角色突破素材" + "label": "----------------------------------\n\n【角色突破素材】" }, { "name": "Fishing", "type": "checkbox", - "label": "-----------------\n\n鱼饵鱼类" + "label": "----------------------------------\n\n【鱼饵、鱼类】" }, { "name": "Gems", "type": "checkbox", - "label": "-----------------\n\n宝石" + "label": "----------------------------------\n\n【宝石】" }, { "name": "Talent", "type": "checkbox", - "label": "-----------------\n\n角色天赋素材" + "label": "----------------------------------\n\n【角色天赋素材】" }, { "name": "WeaponAscension", "type": "checkbox", - "label": "-----------------\n\n武器突破素材" + "label": "----------------------------------\n\n【武器突破素材】" }, { "name": "ImageDelay", "type": "input-text", - "label": "数字太小可能无法识别,用?代替\n=============\n识图基准时间(默认:0 毫秒)" + "label": "数字太小可能无法识别,用?代替\n====================\n识图基准时间(默认:10 毫秒)" } ] \ No newline at end of file