js,背包统计内存优化 (#1358)

This commit is contained in:
JJMdzh
2025-07-16 23:47:39 +08:00
committed by GitHub
parent 6453625ebb
commit a522fc7c0a
4 changed files with 81 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name 背包统计采集系统 // @name 背包统计采集系统
// @version 2.30 // @version 2.40
// @description 识别路径文件,根据材料数量,自动执行路线,或者主动选择材料类别,统计材料数量 // @description 识别路径文件,根据材料数量,自动执行路线,或者主动选择材料类别,统计材料数量
// @author 吉吉喵 // @author 吉吉喵
// @match 原神版本5.6BGI 版本0.44.8 // @match 原神版本5.6BGI 版本0.44.8
@@ -79,4 +79,5 @@
+ v2.27 修复计算材料数错误、目标数量临界值、"3"识别成"三"等bug + v2.27 修复计算材料数错误、目标数量临界值、"3"识别成"三"等bug
+ v2.28 材料更变时初始数量更新正常记录排除0位移和0数量的路径记录(可能是卡路径需手动根据0记录去甄别)新增材料名0后缀本地记录新增背包弹窗识别 + v2.28 材料更变时初始数量更新正常记录排除0位移和0数量的路径记录(可能是卡路径需手动根据0记录去甄别)新增材料名0后缀本地记录新增背包弹窗识别
+ v2.29 新增排除提示;调整平均时间成本计算;过滤掉差异较大的记录; + v2.29 新增排除提示;调整平均时间成本计算;过滤掉差异较大的记录;
+ v2.30 更改路径专注模式默认值加log提示去除注释掉的调试log背包材料统计更改名为背包统计采集系统 + v2.30 更改路径专注模式默认值加log提示去除注释掉的调试log背包材料统计更改名为背包统计采集系统
+ v2.40 优化背包识别时占用的内存;增加通知;

View File

@@ -3,6 +3,7 @@ const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.Target
const OCRdelay = Math.min(50, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长 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))); // 耗时和材料数量的比值,即一个材料多少秒 const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); // 耗时和材料数量的比值,即一个材料多少秒
const notify = settings.notify || false;
// 定义映射表"unselected": "反选材料分类", // 定义映射表"unselected": "反选材料分类",
const material_mapping = { const material_mapping = {
"General": "一般素材", "General": "一般素材",
@@ -97,7 +98,7 @@ const selected_materials_array = Object.keys(finalSettings)
}; };
// OCR识别文本 // OCR识别文本
async function recognizeText(ocrRegion, timeout = 10000, retryInterval = 20, maxAttempts = 10, maxFailures = 3) { async function recognizeText(ocrRegion, timeout = 10000, retryInterval = 20, maxAttempts = 10, maxFailures = 3, cachedFrame = null) {
let startTime = Date.now(); let startTime = Date.now();
let retryCount = 0; let retryCount = 0;
let failureCount = 0; // 用于记录连续失败的次数 let failureCount = 0; // 用于记录连续失败的次数
@@ -117,11 +118,11 @@ const selected_materials_array = Object.keys(finalSettings)
"g": "9", "q": "9", "": "9", "g": "9", "q": "9", "": "9",
}; };
const ra = cachedFrame || captureGameRegion();
while (Date.now() - startTime < timeout && retryCount < maxAttempts) { while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
let captureRegion = captureGameRegion();
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.85; // 适当降低阈值以提高速度 ocrObject.threshold = 0.85; // 适当降低阈值以提高速度
let resList = captureRegion.findMulti(ocrObject); let resList = ra.findMulti(ocrObject);
if (resList.count === 0) { if (resList.count === 0) {
failureCount++; failureCount++;
@@ -302,6 +303,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
// 扫描背包中的材料 // 扫描背包中的材料
for (let scroll = 0; scroll <= pageScrollCount; scroll++) { for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
const ra = captureGameRegion();
if (!foundPriorityMaterial) { if (!foundPriorityMaterial) {
for (const { category, name } of priorityMaterialNames) { for (const { category, name } of priorityMaterialNames) {
if (recognizedMaterials.has(name)) { if (recognizedMaterials.has(name)) {
@@ -318,7 +320,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, startY, columnWidth, columnHeight); const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.8; // 设置识别阈值 recognitionObject.threshold = 0.8; // 设置识别阈值
const result = captureGameRegion().find(recognitionObject); const result = ra.find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) { if (result.isExist() && result.x !== 0 && result.y !== 0) {
foundPriorityMaterial = true; // 标记找到前位材料 foundPriorityMaterial = true; // 标记找到前位材料
log.info(`发现当前或后位材料: ${name},开始全列扫描`); log.info(`发现当前或后位材料: ${name},开始全列扫描`);
@@ -340,7 +342,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight); const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.85; recognitionObject.threshold = 0.85;
const result = captureGameRegion().find(recognitionObject); const result = ra.find(recognitionObject);
await sleep(imageDelay); await sleep(imageDelay);
if (result.isExist() && result.x !== 0 && result.y !== 0) { if (result.isExist() && result.x !== 0 && result.y !== 0) {
@@ -353,7 +355,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
width: 66 + 2 * tolerance, width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance height: 22 + 2 * tolerance
}; };
const ocrResult = await recognizeText(ocrRegion, 1000, OCRdelay, 10, 3); const ocrResult = await recognizeText(ocrRegion, 1000, 10, 10, 3);
materialInfo.push({ name, count: ocrResult.success ? ocrResult.text : "?" }); materialInfo.push({ name, count: ocrResult.success ? ocrResult.text : "?" });
if (!hasFoundFirstMaterial) { if (!hasFoundFirstMaterial) {
@@ -395,7 +397,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
// 检查是否到达最后一页 // 检查是否到达最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26); const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.8; sliderBottomRo.threshold = 0.8;
const sliderBottomResult = captureGameRegion().find(sliderBottomRo); const sliderBottomResult = ra.find(sliderBottomRo);
if (sliderBottomResult.isExist()) { if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!"); log.info("已到达最后一页!");
shouldEndScan = true; shouldEndScan = true;
@@ -471,12 +473,13 @@ const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync
const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38); const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38);
// 定义一个函数用于识别图像 // 定义一个函数用于识别图像
async function recognizeImage(recognitionObject, timeout = 5000) { async function recognizeImage(recognitionObject, timeout = 5000, cachedFrame=null) {
let startTime = Date.now(); let startTime = Date.now();
const ra = cachedFrame || captureGameRegion();
while (Date.now() - startTime < timeout) { while (Date.now() - startTime < timeout) {
try { try {
// 尝试识别图像 // 尝试识别图像
const imageResult = captureGameRegion().find(recognitionObject); const imageResult = ra.find(recognitionObject);
if (imageResult.isExist() && imageResult.x !== 0 && imageResult.y !== 0) { if (imageResult.isExist() && imageResult.x !== 0 && imageResult.y !== 0) {
return { success: true, x: imageResult.x, y: imageResult.y }; return { success: true, x: imageResult.x, y: imageResult.y };
} }
@@ -675,8 +678,9 @@ async function MaterialPath(materialCategoryMap) {
// 自定义 basename 函数 // 自定义 basename 函数
function basename(filePath) { function basename(filePath) {
const lastSlashIndex = filePath.lastIndexOf('\\'); // 或者使用 '/',取决于你的路径分隔符 if (typeof filePath !== 'string') throw new Error('Invalid file path');
return filePath.substring(lastSlashIndex + 1); const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
return filePath.substring(lastSlash + 1);
} }
// 检查路径是否存在 // 检查路径是否存在
function pathExists(path) { function pathExists(path) {
@@ -1241,6 +1245,11 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
// 假设 flattenedLowCountMaterials 是一个全局变量或在外部定义的变量 // 假设 flattenedLowCountMaterials 是一个全局变量或在外部定义的变量
let currentMaterialName = null; // 用于记录当前材料名 let currentMaterialName = null; // 用于记录当前材料名
// 全局累积差值统计(记录所有材料的总变化量)
const globalAccumulatedDifferences = {};
// 按材料分类的累积差值统计(记录每种材料的累计变化)
const materialAccumulatedDifferences = {};
// 遍历所有路径文件 // 遍历所有路径文件
for (const { path: pathingFilePath, resourceName } of allPaths) { for (const { path: pathingFilePath, resourceName } of allPaths) {
const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径 const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径
@@ -1281,17 +1290,28 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
// 输出 resourceCategoryMap 以供调试 // 输出 resourceCategoryMap 以供调试
log.info(`resourceCategoryMap: ${JSON.stringify(resourceCategoryMap, null, 2)}`); log.info(`resourceCategoryMap: ${JSON.stringify(resourceCategoryMap, null, 2)}`);
// 如果材料名发生变化,更新 flattenedLowCountMaterials // 如果材料名发生变化,更新 flattenedLowCountMaterials
if (currentMaterialName !== resourceName) { if (currentMaterialName !== resourceName) {
currentMaterialName = resourceName; // 更新当前材料名 // 材料名变更前,输出上一材料的累积差值并通知
// 调用背包材料统计(获取当前材料数量) if (currentMaterialName && materialAccumulatedDifferences[currentMaterialName]) {
const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); const prevDiffs = materialAccumulatedDifferences[currentMaterialName];
// 展平数组并按数量从小到大排序 log.info(`材料[${currentMaterialName}]收集完成,累积差值:${JSON.stringify(prevDiffs, null, 2)}`);
flattenedLowCountMaterials = updatedLowCountMaterials if (notify) {
.flat() notification.Send(`材料[${currentMaterialName}]收集完成,累计获取:${JSON.stringify(prevDiffs, null, 2)}`);
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); }
log.info(`材料名变更,更新了 flattenedLowCountMaterials`); }
} currentMaterialName = resourceName; // 更新当前材料名
// 调用背包材料统计(获取当前材料数量)
const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap);
// 展平数组并按数量从小到大排序
flattenedLowCountMaterials = updatedLowCountMaterials
.flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
log.info(`材料名变更,更新了 flattenedLowCountMaterials`);
// 初始化当前材料的累积差值记录
materialAccumulatedDifferences[resourceName] = {};
}
// 记录开始时间 // 记录开始时间
const startTime = new Date().toLocaleString(); const startTime = new Date().toLocaleString();
@@ -1321,22 +1341,34 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
.flat() .flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
// 提取更新后的低数量材料的名称
const updatedLowCountMaterialNames = flattenedUpdatedMaterialCounts.map(material => material.name);
// 创建一个映射,用于存储更新前后的数量差值 // 创建一个映射,用于存储更新前后的数量差值
const materialCountDifferences = {}; const materialCountDifferences = {};
// 遍历更新后的材料数量,计算差值 // 遍历更新后的材料数量,计算差值
flattenedUpdatedMaterialCounts.forEach(updatedMaterial => { flattenedUpdatedMaterialCounts.forEach(updatedMaterial => {
const originalMaterial = flattenedLowCountMaterials.find(material => material.name === updatedMaterial.name); const originalMaterial = flattenedLowCountMaterials.find(material => material.name === updatedMaterial.name);
if (originalMaterial) { if (originalMaterial) {
const originalCount = parseInt(originalMaterial.count, 10); const originalCount = parseInt(originalMaterial.count, 10);
const updatedCount = parseInt(updatedMaterial.count, 10); const updatedCount = parseInt(updatedMaterial.count, 10);
const difference = updatedCount - originalCount; const difference = updatedCount - originalCount;
materialCountDifferences[updatedMaterial.name] = difference; materialCountDifferences[updatedMaterial.name] = difference;
}
}); // 更新全局累积差值
if (globalAccumulatedDifferences[updatedMaterial.name]) {
globalAccumulatedDifferences[updatedMaterial.name] += difference;
} else {
globalAccumulatedDifferences[updatedMaterial.name] = difference;
}
// 更新当前材料的累积差值
if (materialAccumulatedDifferences[resourceName][updatedMaterial.name]) {
materialAccumulatedDifferences[resourceName][updatedMaterial.name] += difference;
} else {
materialAccumulatedDifferences[resourceName][updatedMaterial.name] = difference;
}
}
});
// 更新 flattenedLowCountMaterials 为最新的材料数量 // 更新 flattenedLowCountMaterials 为最新的材料数量
flattenedLowCountMaterials = flattenedLowCountMaterials.map(material => { flattenedLowCountMaterials = flattenedLowCountMaterials.map(material => {
@@ -1430,7 +1462,8 @@ async function monitorDisplacement(monitoring, resolve) {
} }
// 识图点击主逻辑 // 识图点击主逻辑
async function imageClick() {
async function imageClick(cachedFrame = null) {
// 定义包含多个文件夹的根目录 // 定义包含多个文件夹的根目录
const rootDir = "assets/imageClick"; const rootDir = "assets/imageClick";
@@ -1500,12 +1533,13 @@ async function imageClick() {
} }
// 在屏幕上查找并点击图标 // 在屏幕上查找并点击图标
const ra = cachedFrame || captureGameRegion();
for (const foundRegion of foundRegions) { for (const foundRegion of foundRegions) {
const tolerance = 1; // 容错区间 const tolerance = 1; // 容错区间
const iconMat = file.readImageMatSync(`${iconDir}/${foundRegion.iconName}`); const iconMat = file.readImageMatSync(`${iconDir}/${foundRegion.iconName}`);
const recognitionObject = RecognitionObject.TemplateMatch(iconMat, foundRegion.region.x - tolerance, foundRegion.region.y - tolerance, foundRegion.region.width + 2 * tolerance, foundRegion.region.height + 2 * tolerance); const recognitionObject = RecognitionObject.TemplateMatch(iconMat, foundRegion.region.x - tolerance, foundRegion.region.y - tolerance, foundRegion.region.width + 2 * tolerance, foundRegion.region.height + 2 * tolerance);
recognitionObject.threshold = 0.9; // 设置识别阈值为 0.9 recognitionObject.threshold = 0.9; // 设置识别阈值为 0.9
const result = captureGameRegion().find(recognitionObject); const result = ra.find(recognitionObject);
if (result.isExist()) { if (result.isExist()) {
const x = Math.round(foundRegion.region.x + foundRegion.region.width / 2); const x = Math.round(foundRegion.region.x + foundRegion.region.width / 2);
const y = Math.round(foundRegion.region.y + foundRegion.region.height / 2); const y = Math.round(foundRegion.region.y + foundRegion.region.height / 2);

View File

@@ -1,13 +1,12 @@
{ {
"manifest_version": 1, "manifest_version": 1,
"name": "背包统计采集系统", "name": "背包统计采集系统",
"version": "2.30", "version": "2.40",
"bgi_version": "0.44.8", "bgi_version": "0.44.8",
"description": "默认四行为一页模板匹配材料OCR识别数量。\n数字太小可能无法识别用?代替。\n目前支持采集材料的数量统计+路径CD管理。", "description": "默认四行为一页模板匹配材料OCR识别数量。\n数字太小可能无法识别用?代替。\n目前支持采集材料的数量统计+路径CD管理。",
"authors": [ "authors": [
{ {
"name": "吉吉喵", "name": "吉吉喵"
"links": "https://github.com/JJMdzh"
} }
], ],
"settings_ui": "settings.json", "settings_ui": "settings.json",

View File

@@ -12,7 +12,12 @@
{ {
"name": "TimeCost", "name": "TimeCost",
"type": "input-text", "type": "input-text",
"label": "====================\n时间成本秒\n1数量的材料平均耗时,默认最多30" "label": "====================\n时间成本秒\n一单位材料平均耗时默认30"
},
{
"name": "notify",
"type": "checkbox",
"label": "----------------------------------\n是否发送通知。默认否\n需在BGI开启JS通知并设置通知地址"
}, },
{ {
"name": "onlyPathing", "name": "onlyPathing",
@@ -23,11 +28,6 @@
"否", "否",
] ]
}, },
{
"name": "unselected",
"type": "checkbox",
"label": "====================\n反选下述【材料分类】"
},
{ {
"name": "Smithing", "name": "Smithing",
"type": "checkbox", "type": "checkbox",