js,背包+路径CD,更快 (#907)

v2.25 当前、后位材料识别(加速扫描), 新增只扫描路径材料名选项(内存占用更小)
This commit is contained in:
JJMdzh
2025-05-23 16:07:00 +08:00
committed by GitHub
parent 83073b273d
commit d99c7e56af
4 changed files with 263 additions and 221 deletions

View File

@@ -62,10 +62,12 @@
+ v1.3 加速寻找(前位材料识别)
+ v1.31本地保存调整
+ v1.32 新增后位材料识别
+ v2.0 多组材料多个分类 开发版 - 前、后位材料识别
+ v2.0 多组材料多个分类 开发版
- v2.0 前、后位材料识别
+ v2.1 CD管理版
+ v2.2 路径顺序 材料数量优化
+ v2.21 储存路径修改
+ v2.22 精简log
+ v2.23 优化部分函数
+ v2.24 修复不能空路径使用背包统计功能等bug
+ v2.25 当前、后位材料识别(加速扫描), 新增只扫描路径材料名选项(内存占用更小)

View File

@@ -1,6 +1,7 @@
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))); // 识图基准时长
// 定义映射表"unselected": "反选材料分类",
const material_mapping = {
"General": "一般素材",
@@ -16,6 +17,8 @@ const material_mapping = {
"Talent": "角色天赋素材",
"WeaponAscension": "武器突破素材"
}
const isOnlyPathing = settings.onlyPathing === "否" ? false : true;
// 检查是否启用反选功能
const isUnselected = settings.unselected === true;
@@ -173,271 +176,290 @@ const selected_materials_array = Object.keys(settings)
await sleep(100);
}
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 frontPriorityMaterials = Object.keys(materialPriority)
.filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType);
// 获取所有优先级更低的材料分类(后位材料)
const backPriorityMaterials = Object.keys(materialPriority)
.filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType);
// 合并当前和后位材料分类
const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料
return finalFilteredMaterials
}
// 扫描材料
async function scanMaterials(materialsCategory, materialCategoryMap) {
async function scanMaterials(materialsCategory, materialCategoryMap, targetResourceName = false, targetMaterialNames = []) {
// 获取当前+后位材料名单
const priorityMaterialNames = [];
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
for (const category of finalFilteredMaterials) {
const materialIconDir = `assets/images/${category}`;
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
priorityMaterialNames.push({ category, name });
}
}
// 根据材料分类获取对应的材料图片文件夹路径
const materialIconDir = `assets/images/${materialsCategory}`;
// 使用 ReadPathSync 读取所有材料图片路径
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
for (const filePath of materialIconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
materialCategories.push({ name: name, filePath: filePath });
allMaterials.add(name); // 将材料名称添加到集合中
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
const materialImages = {}; // 用于缓存加载的图片
// 检查 materialCategoryMap 中当前分类的数组是否为空
const categoryMaterials = materialCategoryMap[materialsCategory] || [];
const shouldScanAllMaterials = categoryMaterials.length === 0; // 如果为空,则扫描所有材料
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
// 如果 materialCategoryMap 中当前分类的数组不为空
// 且当前材料名称不在指定的材料列表中,则跳过加载
if (isOnlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) {
continue;
}
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
// 扫描背包中的材料
const tolerance = 1; // 容错区间
const startX = 117;
const startY = 121;
const OffsetWidth = 147;
const columnWidth = 123;
const columnHeight = 750;
const maxColumns = 8;
// 用于存储图片名和材料数量的数组
const materialInfo = [];
const unmatchedMaterialNames = new Set();// 使用 Set 来存储未匹配的材料名称,确保不重复
// 是否已经开始计时
let hasFoundFirstMaterial = false;
// 记录上一次发现材料的时间
let lastFoundTime = null;
// 初始化标志变量,确保在整个扫描过程中保持状态
// let foundPriorityMaterial = false;
let shouldEndScan = false;
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
// log.info(`第 ${scroll+1} 页`);
// 随机选择一句俏皮话
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
];
// 创建一个数组,用于存储未使用的俏皮话
let tempPhrases = [...scanPhrases];
// 打乱数组顺序,确保随机性
tempPhrases.sort(() => Math.random() - 0.5);
// 记录扫描开始时间
let phrasesStartTime = Date.now();
for (let column = 0; column < maxColumns; column++) {
const scanX = startX + column * OffsetWidth;
for (let i = 0; i < materialCategories.length; i++) {
const { name, filePath } = materialCategories[i];
if (recognizedMaterials.has(name)) {
materialCategories.splice(i, 1); // 从数组中移除已识别的材料
i--; // 调整索引
continue; // 如果已经识别过,跳过
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.85; // 设置识别阈值为 0.85
const result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
recognizedMaterials.add(name); // 标记为已识别
await moveMouseTo(result.x, result.y); // 移动鼠标至图片
const ocrRegion = {
x: result.x - tolerance,
y: result.y + 97 - tolerance,
width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance
};
const ocrResult = await recognizeText(ocrRegion, 1000, OCRdelay, 10, 3);
if (ocrResult.success) {
materialInfo.push({ name: name, count: ocrResult.text });
} else {
log.warn(`芝麻大的数看不清(>ε<)`);
materialInfo.push({ name: name, count: "?" });
}
// 如果是第一次发现材料,开始计时
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
// 更新上一次发现材料的时间
lastFoundTime = Date.now();
}
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
materialCategories.push({ name, filePath });
allMaterials.add(name); // 将材料名称添加到集合中
materialImages[name] = mat; // 缓存图片
}
// 每2秒输出一句俏皮话
const phrasesTime = Date.now();
if (phrasesTime - phrasesStartTime >= 2000) {
// 输出当前数组的第一句俏皮话
const selectedPhrase = tempPhrases.shift();
log.info(selectedPhrase);
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
const unmatchedMaterialNames = new Set(); // 未匹配的材料名称
const materialInfo = []; // 存储材料名称和数量
// 如果数组为空,重新加载并打乱所有俏皮话
if (tempPhrases.length === 0) {
tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5);
// 扫描参数
const tolerance = 1;
const startX = 117;
const startY = 121;
const OffsetWidth = 147;
const columnWidth = 123;
const columnHeight = 750;
const maxColumns = 8;
// 扫描状态
let hasFoundFirstMaterial = false;
let lastFoundTime = null;
let shouldEndScan = false;
let foundPriorityMaterial = false;
// 俏皮话逻辑
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
];
let tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性
let phrasesStartTime = Date.now();
// 扫描背包中的材料
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
if (!foundPriorityMaterial) {
for (const { category, name } of priorityMaterialNames) {
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const filePath = `assets/images/${category}/${name}.png`;
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.8; // 设置识别阈值
const result = captureGameRegion().find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
foundPriorityMaterial = true; // 标记找到前位材料
log.info(`发现当前或后位材料: ${name},开始全列扫描`);
break; // 发现前位材料后,退出当前循环
}
}
}
if (foundPriorityMaterial) {
for (let column = 0; column < maxColumns; column++) {
const scanX = startX + column * OffsetWidth;
for (let i = 0; i < materialCategories.length; i++) {
const { name } = materialCategories[i];
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const mat = materialImages[name];
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.85;
const result = captureGameRegion().find(recognitionObject);
await sleep(Imagedelay);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
recognizedMaterials.add(name);
await moveMouseTo(result.x, result.y);
const ocrRegion = {
x: result.x - tolerance,
y: result.y + 97 - tolerance,
width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance
};
const ocrResult = await recognizeText(ocrRegion, 1000, OCRdelay, 10, 3);
materialInfo.push({ name, count: ocrResult.success ? ocrResult.text : "?" });
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
lastFoundTime = Date.now();
}
}
}
}
}
// 每2秒输出一句俏皮话
const phrasesTime = Date.now();
if (phrasesTime - phrasesStartTime >= 2000) {
const selectedPhrase = tempPhrases.shift();
log.info(selectedPhrase);
if (tempPhrases.length === 0) {
tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5);
}
phrasesStartTime = phrasesTime;
}
// 检查材料识别情况
// 检查是否结束扫描
if (recognizedMaterials.size === allMaterials.size) {
log.info("所有材料均已识别!");
shouldEndScan = true;
break; // 立即退出当前循环
break;
}
// 如果已经发现过材料检查是否超过3秒未发现新的材料
if (hasFoundFirstMaterial) {
const currentTime = Date.now();
if (currentTime - lastFoundTime > 5000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break; // 立即退出当前循环
}
// 如果未超过3秒继续扫描无需额外操作
if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 5000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break;
}
// 检查是否已经滑到最后一页
// 检查是否到最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.8;
const sliderBottomResult = captureGameRegion().find(sliderBottomRo);
if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!");
shouldEndScan = true;
break; // 如果识别到滑动条底部,终止滑动
break;
}
// 如果还没有到达最后一页,继续滑页
// 滑动到下一
if (scroll < pageScrollCount) {
await scrollPage(680, 10, 5);
await sleep(10); // 滑动后等待10毫秒
await sleep(10);
}
}
const lowCountMaterials = filterLowCountMaterials(materialInfo, materialCategoryMap);
// log.info(`低于目标的导入材料名 ${JSON.stringify(lowCountMaterials, null, 2)}`);
// 检查是否需要结束扫描
if (shouldEndScan) {
// 输出识别到的材料数量
log.info(`共识别到 ${recognizedMaterials.size} 种材料`);
const now = new Date(); // 获取当前时间
const formattedTime = now.toLocaleString(); // 使用本地时间格式化
const allMaterialsArray = Array.from(allMaterials);
// 过滤 allMaterials找出不在 recognizedMaterials 中的材料名称
for (const name of allMaterials) {
if (!recognizedMaterials.has(name)) {
unmatchedMaterialNames.add(name); // 使用 Set 的 add 方法添加名称
}
// 处理未匹配的材料
for (const name of allMaterials) {
if (!recognizedMaterials.has(name)) {
unmatchedMaterialNames.add(name);
}
const unmatchedMaterialNamesArray = Array.from(unmatchedMaterialNames);
}
// 准备日志内容
const logContent = `
// 日志记录
const now = new Date();
const formattedTime = now.toLocaleString();
const scanMode = shouldScanAllMaterials ? "全材料扫描" : "指定材料扫描";
const logContent = `
${formattedTime}
${materialsCategory} 种类: ${recognizedMaterials.size} 数量:
${scanMode} - ${materialsCategory} 种类: ${recognizedMaterials.size} 数量:
${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")}
未匹配的材料 种类: ${unmatchedMaterialNamesArray.length} 数量:
${unmatchedMaterialNamesArray.join(",")}
未匹配的材料 种类: ${unmatchedMaterialNames.size} 数量:
${Array.from(unmatchedMaterialNames).join(",")}
`;
// 按材料分类类别分文件记录
// 写入历史记录文件
const categoryFilePath = `history_record/${materialsCategory}.txt`;
let categoryLogContent = `${logContent}\n\n`; // 添加换行分隔
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`;
const latestFilePath = "latest_record.txt";
await writeLog(categoryFilePath, logContent);
await writeLog(overwriteFilePath, logContent);
await writeLog(latestFilePath, logContent);
// 返回结果
return filterLowCountMaterials(materialInfo, materialCategoryMap);
}
async function writeLog(filePath, logContent) {
try {
// 读取现有文件内容
const existingContent = file.readTextSync(categoryFilePath);
// 按记录分隔,假设每条记录之间用两个换行符分隔
const existingContent = file.readTextSync(filePath);
const records = existingContent.split("\n\n");
// 截取最新的365个记录
const latestRecords = records.slice(-365).join("\n\n");
categoryLogContent = `${logContent}\n\n${latestRecords}`; // 将新内容拼接到最前面
const finalContent = `${logContent}\n\n${latestRecords}`;
const result = file.WriteTextSync(filePath, finalContent, false);
if (result) {
log.info(`成功将日志写入文件 ${filePath}`);
} else {
log.error(`写入文件 ${filePath} 失败`);
}
} catch (error) {
// 如果文件不存在,直接使用新内容
log.warn(`文件 ${categoryFilePath} 不存在,将创建新文件`);
}
// 写回文件
const categoryResult = file.WriteTextSync(categoryFilePath, categoryLogContent, false); // 覆盖模式
if (categoryResult) {
log.info(`成功将 ${materialsCategory} 的材料写入历史文件`);
} else {
log.error(`写入 ${materialsCategory} 的本地文件失败`);
}
// 按材料分类类别分文件覆写记录
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`;
const overwriteLogContent = `${logContent}
图库的材料 种类: ${allMaterialsArray.length} 数量:
${allMaterialsArray.join(",")}\n\n`; // 添加换行分隔
const overwriteResult = file.WriteTextSync(overwriteFilePath, overwriteLogContent, false); // 覆盖模式
if (overwriteResult) {
log.info(`成功将 ${materialsCategory} 的记录写入覆写文件`);
log.warn(`文件 ${filePath} 不存在,将创建新文件`);
const result = file.WriteTextSync(filePath, logContent, false);
if (result) {
log.info(`成功创建并写入文件 ${filePath}`);
} else {
log.error(`覆写 ${materialsCategory} 到本地文件失败`);
}
// 最新的历史覆写记录
const latestFilePath = "latest_record.txt";
const latestLogContent = `${logContent}
图库的材料 种类: ${allMaterialsArray.length} 数量:
${allMaterialsArray.join(",")}\n\n`; // 添加换行分隔
const latestResult = file.WriteTextSync(latestFilePath, latestLogContent, false); // 覆盖模式
if (latestResult) {
log.info("成功将最新的历史记录写入js根目录");
} else {
log.error("写入最新的历史记录失败");
log.error(`创建文件 ${filePath} 失败`);
}
}
return lowCountMaterials;
}
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
@@ -452,8 +474,8 @@ async function recognizeImage(recognitionObject, timeout = 5000) {
while (Date.now() - startTime < timeout) {
try {
// 尝试识别图像
let imageResult = captureGameRegion().find(recognitionObject);
if (imageResult) {
const imageResult = captureGameRegion().find(recognitionObject);
if (imageResult.isExist() && imageResult.x !== 0 && imageResult.y !== 0) {
// log.info(`成功识别图像,坐标: x=${imageResult.x}, y=${imageResult.y}`);
// log.info(`图像尺寸: width=${imageResult.width}, height=${imageResult.height}`);
return { success: true, x: imageResult.x, y: imageResult.y };
@@ -612,7 +634,7 @@ async function MaterialPath(materialCategoryMap) {
}
let CategoryResult = await recognizeImage(CategoryObject, 2000);
if (CategoryResult.success && CategoryResult.x !== 0 && CategoryResult.y !== 0) {
if (CategoryResult.success) {
log.info(`识别到${materialsCategory} 所在分类。`);
stage = 4; // 进入下一阶段
} else {
@@ -1042,7 +1064,16 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
materialCategoryMap[selectedCategory] = [];
}
});
// log.info(JSON.stringify(materialCategoryMap, null, 2));
// 如果 isOnlyPathing 为 true移除 materialCategoryMap 中的空数组
// 如果 isOnlyPathing 为 true移除 materialCategoryMap 中的空数组
if (isOnlyPathing) {
Object.keys(materialCategoryMap).forEach(category => {
if (materialCategoryMap[category].length === 0) {
delete materialCategoryMap[category];
}
});
}
log.info(JSON.stringify(materialCategoryMap, null, 2));
// 调用背包材料统计
const lowCountMaterialsFiltered = await MaterialPath(materialCategoryMap);

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "背包材料统计",
"version": "2.24",
"version": "2.25",
"bgi_version": "0.44.8",
"description": "默认四行为一页模板匹配材料OCR识别数量。\n数字太小可能无法识别用?代替。\n目前支持采集路线。\n材料种类数量或导入js本地\n图包文件夹images放入assets下\nv2.24修复不能空路径使用背包统计功能等bug",
"authors": [

View File

@@ -9,10 +9,19 @@
"type": "input-text",
"label": "如填入 甜甜花,薄荷,苹果\n-----------------\n\n目标数量默认5000"
},
{
"name": "onlyPathing",
"type": "select",
"label": "上2项可不填\n=============\n只扫描pathing文件夹中材料名\n的数量默认是",
"options": [
"是",
"否",
]
},
{
"name": "unselected",
"type": "checkbox",
"label": "上2项可不填\n=============\n反选材料分类"
"label": "=============\n反选材料分类"
},
{
"name": "Smithing",
@@ -75,8 +84,8 @@
"label": "-----------------\n\n武器突破素材"
},
{
"name": "OcrDelay",
"name": "ImageDelay",
"type": "input-text",
"label": "数字太小可能无法识别,用?代替\n=============\nOCR基准时间(默认:10 毫秒)"
"label": "数字太小可能无法识别,用?代替\n=============\n识图基准时间(默认:0 毫秒)"
}
]