js背包材料统计 修复自动超量名单不生效 (#2490)

修复自动超量名单不生效;ocr代码分块
This commit is contained in:
吉吉喵
2025-12-14 16:08:31 +08:00
committed by GitHub
parent d5bef7c92c
commit 4ef7cbac7f
8 changed files with 437 additions and 341 deletions

View File

@@ -58,93 +58,6 @@ function readtargetTextCategories(targetTextDir) {
}
return materialCategories;
}
// 定义替换映射表
const replacementMap = {
"监": "盐",
"炽": "烬",
"盞": "盏",
"攜": "携",
"於": "于",
"卵": "卯"
};
/**
* 执行OCR识别并匹配目标文本
* @param {string[]} targetTexts - 待匹配的目标文本列表
* @param {Object} xRange - X轴范围 { min: number, max: number }
* @param {Object} yRange - Y轴范围 { min: number, max: number }
* @param {number} timeout - 超时时间(毫秒)默认200ms
* @param {Object} ra - 图像捕获对象(外部传入,需确保已初始化)
* @returns {Promise<Object[]>} 识别结果数组,包含匹配目标的文本及坐标信息
*/
async function performOcr(targetTexts, xRange, yRange, timeout = 10, ra = null) {
// 正则特殊字符转义工具函数(避免替换时的正则语法错误)
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const startTime = Date.now(); // 记录开始时间,用于超时判断
// 2. 计算识别区域宽高xRange.max - xRange.min 为宽度y同理
const regionWidth = xRange.max - xRange.min;
const regionHeight = yRange.max - yRange.min;
if (regionWidth <= 0 || regionHeight <= 0) {
throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`);
}
// 在超时时间内循环重试识别(处理临时识别失败)
while (Date.now() - startTime < timeout) {
try {
// 1. 检查图像捕获对象是否有效
if (!ra) {
throw new Error("图像捕获对象(ra)未初始化");
}
// 3. 执行OCR识别在指定区域内查找多结果
const resList = ra.findMulti(
RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight)
);
// 4. 处理识别结果(文本修正 + 目标匹配)
const results = [];
for (let i = 0; i < resList.count; i++) {
const res = resList[i];
let correctedText = res.text; // 原始识别文本
log.info(`原始识别文本: ${res.text}`);
// 4.1 修正识别错误(替换错误字符)
for (const [wrongChar, correctChar] of Object.entries(replacementMap)) {
const escapedWrong = escapeRegExp(wrongChar); // 转义特殊字符
correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar);
}
// 4.2 检查是否包含任意目标文本(避免重复添加同个结果)
const isTargetMatched = targetTexts.some(target => correctedText.includes(target));
if (isTargetMatched) {
results.push({
text: correctedText,
x: res.x,
y: res.y,
width: res.width,
height: res.height
});
}
}
// 5. 识别成功,返回结果(无论是否匹配到目标,均返回当前识别到的有效结果)
return results;
} catch (error) {
// 识别异常时记录日志,继续重试(直到超时)
log.error(`OCR识别异常将重试: ${error.message}`);
// 短暂等待后重试,避免高频失败占用资源
await sleep(1);
}
await sleep(5); // 每次间隔 5 毫秒
}
// 超时未完成识别,返回空数组
log.warn(`OCR识别超时超过${timeout}ms`);
return [];
}
// const OCRdelay = Math.min(100, Math.max(0, Math.floor(Number(settings.OcrDelay) || 2))); // F识别基准时长
@@ -188,18 +101,25 @@ const ScrollRo = RecognitionObject.TemplateMatch(
*/
async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame = null) {
let lastLogTime = Date.now();
// 记录每个材料的识别次数(文本+坐标 → 计数
const recognitionCount = new Map();
const recognitionCount = new Map(); // 避免误触:文本+Y坐标 → 计数
const ocrScreenshots = []; // 收集最新版performOcr返回的截图统一释放
while (!state.completed && !state.cancelRequested) {
const currentTime = Date.now();
if (currentTime - lastLogTime >= 10000) {
log.info("检测中...");
lastLogTime = currentTime;
}
await sleep(50);
cachedFrame?.dispose();
cachedFrame = captureGameRegion();
try {
while (!state.completed && !state.cancelRequested) {
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();
// 尝试找到 F 图标
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
@@ -211,21 +131,33 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
continue; // 继续下一轮检测
}
// 获取 F 图标的中心点 Y 坐标
let centerYF = fRes.y + fRes.height / 2;
let ocrResults = await performOcr(targetTexts, textxRange, { min: fRes.y - 3, max: fRes.y + 37 }, 10, cachedFrame);
// 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); // 收集截图,避免内存泄漏
// 检查所有目标文本是否在当前页面中
let foundTarget = false;
for (let targetText of targetTexts) {
let targetResult = ocrResults.find(res => res.text.includes(targetText));
if (targetResult) {
// 4. 文本匹配与交互(保留原逻辑,无修改)
let foundTarget = false;
for (const targetText of targetTexts) {
const targetResult = ocrResults.find(res => res.text.includes(targetText));
if (!targetResult) continue;
// 计数防误触(原逻辑)
const materialId = `${targetText}-${targetResult.y}`;
recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1);
let centerYTargetText = targetResult.y + targetResult.height / 2;
if (Math.abs(centerYTargetText - centerYF) <= texttolerance) {
// Y轴对齐判断原逻辑
const centerYTargetText = targetResult.y + targetResult.height / 2;
if (Math.abs(centerYTargetText - (fRes.y + fRes.height / 2)) <= texttolerance) {
if (recognitionCount.get(materialId) >= 1) {
keyPress("F");
log.info(`交互或拾取: ${targetText}`);
@@ -236,20 +168,33 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
break;
}
}
}
if (!foundTarget) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
// 5. 未找到目标则翻滚(保留原逻辑)
if (!foundTarget) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
}
}
} 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) {
break;
log.info("检测任务已取消");
} else if (!state.completed) {
log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本");
}
cachedFrame?.dispose();
}
if (state.cancelRequested) {
log.info("检测任务已取消");
} else if (!state.completed) {
log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本");
}
}

View File

@@ -44,7 +44,19 @@ const materialPriority = {
"武器突破素材": 6,
};
// 2. 数字替换映射表处理OCR识别误差
var numberReplaceMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1", "一": "1",
"Z": "2", "z": "2", "": "2", "二": "2",
"E": "3", "e": "3", "": "3", "三": "3",
"A": "4", "a": "4", "": "4",
"S": "5", "s": "5", "": "5",
"G": "6", "b": "6", "": "6",
"T": "7", "t": "7", "": "7",
"B": "8", "θ": "8", "": "8",
"g": "9", "q": "9", "": "9",
};
// 提前计算所有动态坐标
// 物品区左顶处物品左上角坐标(117,121)
@@ -63,19 +75,6 @@ async function recognizeText(ocrRegion, timeout = 100, retryInterval = 20, maxAt
// const results = [];
const frequencyMap = {}; // 用于记录每个结果的出现次数
const numberReplaceMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1", "一": "1",
"Z": "2", "z": "2", "": "2", "二": "2",
"E": "3", "e": "3", "": "3", "三": "3",
"A": "4", "a": "4", "": "4",
"S": "5", "s": "5", "": "5",
"G": "6", "b": "6", "": "6",
"T": "7", "t": "7", "": "7",
"B": "8", "θ": "8", "": "8",
"g": "9", "q": "9", "": "9",
};
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.85; // 适当降低阈值以提高速度
@@ -296,26 +295,26 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
// 俏皮话逻辑
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
"... 太好啦,有这么多素材!",
"... 不错的珍宝!",
"... 侦查骑士,发现目标!",
"... 嗯哼,意外之喜!",
"... 嗯?",
"... 很好,没有放过任何角落!",
"... 会有烟花材料嘛?",
"... 嗯,这是什么?",
"... 这些宝藏积灰了,先清洗一下",
"... 哇!都是好东西!",
"... 不虚此行!",
"... 瑰丽的珍宝,令人欣喜。",
"... 是对长高有帮助的东西吗?",
"... 嗯!品相卓越!",
"... 虽无法比拟黄金,但终有价值。",
"... 收获不少,可以拿去换几瓶好酒啦。",
"... 房租和伙食费,都有着落啦!",
"... 还不赖。",
"... 荒芜的世界,竟藏有这等瑰宝。",
"... 运气还不错。",
];
let tempPhrases = [...scanPhrases];
@@ -462,45 +461,15 @@ ${Array.from(unmatchedMaterialNames).join(",")}
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存
const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合
if (pathingMode.onlyCategory) {
writeLog(categoryFilePath, logContent);
writeFile(categoryFilePath, logContent);
}
writeLog(overwriteFilePath, logContent);
writeLog(latestFilePath, logContent); // 覆盖模式?
writeFile(overwriteFilePath, logContent);
writeFile(latestFilePath, logContent); // 覆盖模式?
// 返回结果
return materialInfo;
}
function writeLog(filePath, logContent) {
try {
// 1. 读取现有内容(原样读取,不做任何分割处理)
let existingContent = "";
try {
existingContent = file.readTextSync(filePath);
} catch (e) {
// 文件不存在则保持空
}
// 2. 拼接新记录(新记录加在最前面,用两个换行分隔,保留原始格式)
const finalContent = logContent + "\n\n" + existingContent;
// 3. 按行分割保留最近365条完整记录按原始换行分割不过滤
const lines = finalContent.split("\n");
const keepLines = lines.length > 365 * 5 ? lines.slice(0, 365 * 5) : lines; // 假设每条记录最多5行
const result = file.writeTextSync(filePath, keepLines.join("\n"), false);
if (result) {
log.info(`写入成功: ${filePath}`);
} else {
log.error(`写入失败: ${filePath}`);
}
} catch (error) {
// 只在文件完全不存在时创建,避免覆盖
file.writeTextSync(filePath, logContent, false);
log.info(`创建新文件: ${filePath}`);
}
}
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38);
const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38);
@@ -565,7 +534,11 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
});
while (stage <= maxStage) {
let loopCount = 0;
const maxLoopCount = 200; // 合理阈值正常流程约50-100次循环
while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 =====
loopCount++;
switch (stage) {
case 0: // 返回主界面
log.info("返回主界面");
@@ -588,6 +561,8 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
stage = 2; // 进入下一阶段
} else {
log.warn("未识别到背包图标,重新尝试");
// ===== 补充优化:连续回退时释放资源 =====
cachedFrame?.dispose();
stage = 0; // 回退
}
break;
@@ -643,6 +618,8 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
break;
default:
log.error("未知的材料分类");
// ===== 补充优化:异常时释放资源并退出 =====
cachedFrame?.dispose();
stage = 0; // 回退到阶段0
return;
}
@@ -653,7 +630,9 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
stage = 4; // 进入下一阶段
} else {
log.warn("未识别到材料分类图标,重新尝试");
log.warn(`识别结果:${JSON.stringify(CategoryResult)}`);
// log.warn(`识别结果:${JSON.stringify(CategoryResult)}`);
// ===== 补充优化:连续回退时释放资源 =====
cachedFrame?.dispose();
stage = 2; // 回退到阶段2
}
break;
@@ -683,6 +662,14 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) {
}
}
// ===== 补充优化:循环超限处理,防止卡死 =====
if (loopCount > maxLoopCount) {
log.error(`主循环次数超限(${maxLoopCount}次),强制退出`);
cachedFrame?.dispose();
await genshin.returnMainUi();
return [];
}
await genshin.returnMainUi(); // 返回主界面
log.info("扫描流程结束");

View File

@@ -1,35 +1,20 @@
// ===================== 狗粮模式专属函数 =====================
// 1. 狗粮分解配置与OCR区域
// 1. 狗粮分解配置与OCR区域(保留原配置,无修改)
const AUTO_SALVAGE_CONFIG = {
autoSalvage3: settings.autoSalvage3 || "是",
autoSalvage4: settings.autoSalvage4 || "是"
};
const OCR_REGIONS = {
const EXP_OCRREGIONS = {
expStorage: { x: 1472, y: 883, width: 170, height: 34 },
expCount: { x: 1472, y: 895, width: 170, height: 34 }
};
// 2. 数字替换映射表处理OCR识别误差
const numberReplaceMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1", "一": "1",
"Z": "2", "z": "2", "": "2", "二": "2",
"E": "3", "e": "3", "": "3", "三": "3",
"A": "4", "a": "4", "": "4",
"S": "5", "s": "5", "": "5",
"G": "6", "b": "6", "": "6",
"T": "7", "t": "7", "": "7",
"B": "8", "θ": "8", "": "8",
"g": "9", "q": "9", "": "9",
};
// 3. OCR文本处理
function processExpText(text) {
// 处理数字文本:保留原逻辑(复用全局数字替换能力
function processNumberText(text) {
let correctedText = text || "";
let removedSymbols = [];
// 替换错误字符
// 替换错误字符(依赖全局 numberReplaceMap
for (const [wrong, correct] of Object.entries(numberReplaceMap)) {
correctedText = correctedText.replace(new RegExp(wrong, 'g'), correct);
}
@@ -50,64 +35,88 @@ function processExpText(text) {
};
}
// 4. OCR识别EXP
async function recognizeExpRegion(regionName, ra = null, timeout = 2000) {
const ocrRegion = OCR_REGIONS[regionName];
// 4. OCR识别EXP(核心改造:用 performOcr 替代重复OCR逻辑
async function recognizeExpRegion(regionName, initialRa = null, timeout = 2000) {
// 1. 基础校验(保留原逻辑)
const ocrRegion = EXP_OCRREGIONS[regionName];
if (!ocrRegion) {
log.error(`[狗粮OCR] 无效区域:${regionName}`);
return { success: false, expCount: 0 };
return { success: false, expCount: 0, screenshot: null }; // 新增返回截图(便于调试)
}
log.info(`[狗粮OCR] 识别${regionName}x=${ocrRegion.x}, y=${ocrRegion.y}`);
const startTime = Date.now();
let retryCount = 0;
let ocrScreenshot = null; // 存储performOcr返回的有效截图
while (Date.now() - startTime < timeout) {
try {
const ocrResult = ra.find(RecognitionObject.ocr(
ocrRegion.x,
ocrRegion.y,
ocrRegion.width,
ocrRegion.height
));
log.info(`[狗粮OCR] 原始文本:${ocrResult.text}`);
try {
// 2. 转换OCR区域格式ocrRegion(x,y,width,height) → xRange/yRange(min/max)
const xRange = {
min: ocrRegion.x,
max: ocrRegion.x + ocrRegion.width
};
const yRange = {
min: ocrRegion.y,
max: ocrRegion.y + ocrRegion.height
};
if (ocrResult?.text) {
const { processedText, removedSymbols } = processExpText(ocrResult.text);
if (removedSymbols.length > 0) {
log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`);
}
const expCount = processedText ? parseInt(processedText, 10) : 0;
log.info(`[狗粮OCR] ${regionName}结果:${expCount}`);
return { success: true, expCount };
// 3. 调用新版 performOcr自动重截图、资源管理、异常处理
// 目标文本传空数组:识别数字无需匹配特定文本,仅需提取内容
const { results, screenshot } = await performOcr(
[""], // targetTexts空数组数字识别无特定目标
xRange, // 转换后的X轴范围
yRange, // 转换后的Y轴范围
initialRa, // 初始截图(外部传入)
timeout, // 超时时间(复用原参数)
50 // 重试间隔默认50ms比原500ms更灵敏
);
ocrScreenshot = screenshot; // 暂存截图,后续返回或释放
// 4. 处理OCR结果保留原数字处理+日志逻辑)
if (results.length > 0) {
const { originalText, text: correctedText } = results[0]; // 从performOcr拿原始/修正文本
log.info(`[狗粮OCR] 原始文本:${originalText}`); // 保持原日志格式
// 用原processNumberText提纯数字
const { processedText, removedSymbols } = processNumberText(correctedText);
if (removedSymbols.length > 0) {
log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`); // 保留原日志
}
} catch (error) {
retryCount++;
log.warn(`[狗粮OCR] ${regionName}${retryCount}次识别失败:${error.message}`);
const expCount = processedText ? parseInt(processedText, 10) : 0;
log.info(`[狗粮OCR] ${regionName}结果:${expCount}`); // 保留原日志
return { success: true, expCount, screenshot: ocrScreenshot }; // 返回截图(调试用)
}
} catch (error) {
// 捕获performOcr未处理的异常如参数错误
log.error(`[狗粮OCR] ${regionName}识别异常:${error.message}`);
// 异常时释放截图资源
if (ocrScreenshot) {
if (ocrScreenshot.Dispose) ocrScreenshot.Dispose();
else if (ocrScreenshot.dispose) ocrScreenshot.dispose();
}
await sleep(500);
}
// 5. 识别失败/超时(保留原逻辑)
log.error(`[狗粮OCR] ${regionName}超时未识别默认0`);
return { success: false, expCount: 0 };
return { success: false, expCount: 0, screenshot: ocrScreenshot }; // 超时也返回截图(排查用)
}
// 5. 狗粮分解流程
// 5. 狗粮分解流程调整适配recognizeExpRegion的新返回值优化资源释放
async function executeSalvageWithOCR() {
log.info("[狗粮分解] 开始执行分解流程");
let storageExp = 0;
let countExp = 0;
let cachedFrame = null;
let ocrScreenshots = []; // 存储识别过程中产生的截图(统一释放)
try {
keyPress("B"); await sleep(1000);
keyPress("B");
await sleep(1000);
const coords = [
[670, 40], // 打开背包
[660, 1010], // 打开分解
[300, 1020], // 打开分解选项页面
// [200, 150, 500], // 勾选1星狗粮
// [200, 220, 500], // 勾选2星狗粮
[200, 300, 500, AUTO_SALVAGE_CONFIG.autoSalvage3 !== '否'], // 3星按配置
[200, 380, 500, AUTO_SALVAGE_CONFIG.autoSalvage4 !== '否'], // 4星按配置
[340, 1000], // 确认选择
@@ -125,35 +134,64 @@ async function executeSalvageWithOCR() {
await sleep(delay);
log.debug(`[狗粮分解] 点击(${x},${y}),延迟${delay}ms`);
// 分解前识别储存EXP
// 分解前识别储存EXP适配新的recognizeExpRegion返回值
if (x === 660 && y === 1010) {
cachedFrame?.dispose();
// 释放旧缓存帧
if (cachedFrame) {
if (cachedFrame.Dispose) cachedFrame.Dispose();
else if (cachedFrame.dispose) cachedFrame.dispose();
}
// 捕获新帧
cachedFrame = captureGameRegion();
const { expCount } = await recognizeExpRegion("expStorage", cachedFrame, 1000);
// 调用改造后的recognizeExpRegion接收expCount和screenshot
const { expCount, screenshot } = await recognizeExpRegion("expStorage", cachedFrame, 1000);
storageExp = expCount;
ocrScreenshots.push(screenshot); // 收集截图(后续统一释放)
}
// 分解后识别新增EXP
// 分解后识别新增EXP(同上,适配新返回值)
if (x === 340 && y === 1000) {
cachedFrame?.dispose();
if (cachedFrame) {
if (cachedFrame.Dispose) cachedFrame.Dispose();
else if (cachedFrame.dispose) cachedFrame.dispose();
}
cachedFrame = captureGameRegion();
const { expCount } = await recognizeExpRegion("expCount", cachedFrame, 1000);
const { expCount, screenshot } = await recognizeExpRegion("expCount", cachedFrame, 1000);
countExp = expCount;
ocrScreenshots.push(screenshot); // 收集截图
}
}
}
const totalExp = countExp - storageExp; // 分解新增EXP = 分解后 - 分解前
// 计算并返回结果(保留原逻辑)
const totalExp = countExp - storageExp;
log.info(`[狗粮分解] 完成新增EXP${totalExp}(分解前:${storageExp},分解后:${countExp}`);
return { success: true, totalExp: Math.max(totalExp, 0) }; // 避免负数
return { success: true, totalExp: Math.max(totalExp, 0) };
} catch (error) {
log.error(`[狗粮分解] 失败:${error.message}`);
return { success: false, totalExp: 0 };
} finally {
// 最终统一释放所有资源(避免内存泄漏)
// 1. 释放缓存帧
if (cachedFrame) {
if (cachedFrame.Dispose) cachedFrame.Dispose();
else if (cachedFrame.dispose) cachedFrame.dispose();
}
// 2. 释放OCR过程中产生的截图
for (const screenshot of ocrScreenshots) {
if (screenshot) {
if (screenshot.Dispose) screenshot.Dispose();
else if (screenshot.dispose) screenshot.dispose();
}
}
log.debug("[狗粮分解] 所有资源已释放");
}
}
// 6. 判断是否为狗粮资源(关键词匹配
// 6. 判断是否为狗粮资源(保留原逻辑,无修改
function isFoodResource(resourceName) {
const foodKeywords = ["12h狗粮", "24h狗粮"];
return resourceName && foodKeywords.some(keyword => resourceName.includes(keyword));
}
}

View File

@@ -78,43 +78,8 @@ function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.p
return [];
}
}
/*
// 新记录在最下面
async function writeFile(filePath, content, isAppend = false, maxRecords = 365) {
try {
if (isAppend) {
// 读取现有内容(如果文件不存在则为空)
const existingContent = file.readTextSync(filePath) || "";
// 分割成记录数组(过滤空字符串)
const records = existingContent.split("\n\n").filter(Boolean);
// 关键修复将新内容添加到末尾然后只保留最后maxRecords条
const allRecords = [...records, content]; // 新内容放在最后
const latestRecords = allRecords.slice(-maxRecords); // 整体截取最新的maxRecords条
// 拼接成最终内容
const finalContent = latestRecords.join("\n\n");
const result = file.WriteTextSync(filePath, finalContent, false);
// 日志输出(可根据需要启用)
// log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`);
return result;
} else {
// 非追加模式:直接覆盖写入
const result = file.WriteTextSync(filePath, content, false);
// log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`);
return result;
}
} catch (error) {
// 发生错误时尝试创建文件并写入
const result = file.WriteTextSync(filePath, content, false);
log.info(result ? `[新建] 成功创建: ${filePath}` : `[新建] 创建失败: ${filePath}`);
return result;
}
}
*/
// 新记录在最上面20250531
async function writeFile(filePath, content, isAppend = false, maxRecords = 365) {
// 新记录在最上面20250531 isAppend默认就是true追加
function writeFile(filePath, content, isAppend = true, maxRecords = 36500) {
try {
if (isAppend) {
// 读取现有内容,处理文件不存在的情况

View File

@@ -104,17 +104,19 @@ async function preloadImageResources(specificNames) {
});
}
const targetIcon = iconRecognitionObjects[0];
const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y);
manualRegion.width = specialDetectRegion.width;
manualRegion.height = specialDetectRegion.height;
const foundRegions = [{
pictureName: "特殊模块",
iconName: targetIcon.name,
region: manualRegion,
iconDir: iconDir
}];
// 关键修改:遍历所有图标,为每个图标生成识别信息
const foundRegions = []; // 存储所有图标的识别配置
for (const targetIcon of iconRecognitionObjects) { // 遍历每个图标
const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y);
manualRegion.width = specialDetectRegion.width;
manualRegion.height = specialDetectRegion.height;
foundRegions.push({
pictureName: "特殊模块",
iconName: targetIcon.name, // 当前图标的名称
region: manualRegion, // 复用同一个detectRegion区域
iconDir: iconDir
});
}
// log.info(`【${dirName}】特殊模块生成识别区域x=${manualRegion.x}, y=${manualRegion.y}, 宽=${manualRegion.width}, 高=${manualRegion.height}`);
preloadedResources.push({
@@ -251,11 +253,12 @@ async function imageClickBackgroundTask() {
// 遍历所有一级弹窗
for (const currentFirstLevel of firstLevelDirs) {
log.info(`${currentFirstLevel.dirName}】准备识别...`);
// 检查当前一级弹窗是否被触发
const levelResult = await imageClick([currentFirstLevel], null, [currentFirstLevel.dirName], true);
if (levelResult.success) {
log.info(`${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
// log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
const levelStack = [currentFirstLevel];
// 内循环处理内部流程
@@ -335,6 +338,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
detectRegion?.width ?? defaultWidth,
detectRegion?.height ?? defaultHeight
);
// log.info(JSON.stringify(detectRegion, null, 2));
recognitionObject.threshold = 0.85;
const result = await recognizeImage(
@@ -356,12 +360,12 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
log.info(`识别到【${dirName}】弹窗,偏移后位置(${actualX}, ${actualY})`);
if (!popupConfig.isSpecial) {
// log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})`);
// 新增普通点击加循环默认1次0间隔与原逻辑一致
const clickCount = popupConfig.loopCount;
const clickDelay = popupConfig.loopDelay;
for (let i = 0; i < clickCount; i++) {
await click(actualX, actualY); // 保留原始点击逻辑
// log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}次`);
if (i < clickCount - 1) await sleep(clickDelay); // 非最后一次加间隔
}
} else {
@@ -370,9 +374,10 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
const targetKey = popupConfig.keyCode || "VK_SPACE";
// 新增key_press用循环默认3次1000ms间隔与原硬编码逻辑一致
const pressCount = popupConfig.loopCount || 3;
const pressDelay = popupConfig.loopDelay || 1000;
const pressDelay = popupConfig.loopDelay || 500;
for (let i = 0; i < pressCount; i++) {
keyPress(targetKey); // 保留原始按键逻辑
log.info(`${dirName}】弹窗触发按键【${targetKey}${i+1}`);
if (i < pressCount - 1) await sleep(pressDelay); // 非最后一次加间隔
}
log.info(`${dirName}】弹窗触发按键【${targetKey}】,共${pressCount}次,间隔${pressDelay}ms`);
@@ -438,6 +443,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u
const defaultDelay = popupConfig.loopDelay;
for (let i = 0; i < defaultCount; i++) {
await click(actualX, actualY); // 保留原始默认点击逻辑
log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}`);
if (i < defaultCount - 1) await sleep(defaultDelay); // 非最后一次加间隔
}
isAnySuccess = true;

View File

@@ -0,0 +1,122 @@
// 定义替换映射表
const replacementMap = {
"监": "盐",
"炽": "烬",
"盞": "盏",
"攜": "携",
"於": "于",
"卵": "卯"
};
/**
* 执行OCR识别并匹配目标文本失败自动重截图返回结果+有效截图)
* @param {string[]} targetTexts - 待匹配的目标文本列表
* @param {Object} xRange - X轴范围 { min: number, max: number }
* @param {Object} yRange - Y轴范围 { min: number, max: number }
* @param {Object} ra - 初始图像捕获对象(外部传入,需确保已初始化)
* @param {number} timeout - 超时时间(毫秒)默认200ms
* @param {number} interval - 重试间隔(毫秒)默认50ms
* @returns {Promise<{
* results: Object[], // 识别结果数组(含文本、坐标)
* screenshot: Object // 有效截图(成功时用的截图/超时前最后一次截图)
* }>}
*/
async function performOcr(targetTexts, xRange, yRange, ra = null, timeout = 200, interval = 50) {
// 正则特殊字符转义工具函数(避免替换时的正则语法错误)
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const startTime = Date.now(); // 记录开始时间,用于超时判断
let currentScreenshot = ra; // 跟踪当前有效截图(初始为外部传入的原图)
// 1. 初始参数校验(提前拦截无效输入)
if (!currentScreenshot) {
throw new Error("初始图像捕获对象(ra)未初始化,请传入有效截图");
}
const regionWidth = xRange.max - xRange.min;
const regionHeight = yRange.max - yRange.min;
if (regionWidth <= 0 || regionHeight <= 0) {
throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`);
}
// 在超时时间内循环重试识别(处理临时失败,自动重截图)
while (Date.now() - startTime < timeout) {
// 额外增加空值检查防止currentScreenshot变为null
if (!currentScreenshot) {
log.error("currentScreenshot为null尝试重新捕获");
currentScreenshot = captureGameRegion();
await sleep(interval);
continue;
}
try {
// 2. 执行OCR识别基于当前有效截图的指定区域
const resList = currentScreenshot.findMulti(
RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight)
);
// 3. 处理识别结果(文本修正 + 目标匹配)
const results = [];
for (let i = 0; i < resList.count; i++) {
const res = resList[i];
let correctedText = res.text; // 修正后的文本
const originalText = res.text; // 保留原始识别文本(便于调试)
log.debug(`OCR原始文本: ${res.text}`);
// 3.1 修正OCR常见错误基于替换映射表
for (const [wrongChar, correctChar] of Object.entries(replacementMap)) {
const escapedWrong = escapeRegExp(wrongChar);
correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar);
}
// 3.2 匹配目标文本(避免重复添加同一结果)
const isTargetMatched = targetTexts.some(target => correctedText.includes(target));
if (isTargetMatched) {
results.push({
text: correctedText, // 最终修正后的文本
originalText: originalText, // 原始识别文本(调试用)
x: res.x, y: res.y, // 文本在截图中的X/Y坐标
width: res.width, height: res.height // 文本区域尺寸
});
}
}
// 4. 识别成功:返回「结果数组 + 本次成功用的截图」
// log.info(`OCR识别完成匹配到${results.length}个目标文本`);
return {
results: results,
screenshot: currentScreenshot // 成功截图(与结果对应的有效画面)
};
} catch (error) {
// 5. 识别失败:释放旧截图→重新捕获→更新当前截图
if (currentScreenshot) {
// 检查是否存在释放方法,支持不同可能的命名
if (typeof currentScreenshot.Dispose === 'function') {
currentScreenshot.Dispose();
} else if (typeof currentScreenshot.dispose === 'function') {
currentScreenshot.dispose();
}
log.debug("已释放旧截图资源,准备重新捕获");
}
// 重新捕获后增加null校验
currentScreenshot = captureGameRegion();
if (!currentScreenshot) {
log.error("重新捕获截图失败返回了null值");
}
log.error(`OCR识别异常已重新截图将重试: ${error.message}`);
await sleep(5); // 短暂等待避免高频截图占用CPU/内存
}
await sleep(interval); // 每次重试间隔默认50ms
}
// 6. 超时未成功:返回「空结果 + 超时前最后一次截图」
log.warn(`OCR识别超时超过${timeout}ms`);
return {
results: [],
screenshot: currentScreenshot // 超时前最后一次有效截图(可用于排查原因)
};
}

View File

@@ -34,6 +34,7 @@ const CONSTANTS = {
// 引入外部脚本(源码不变)
// ==============================================
eval(file.readTextSync("lib/file.js"));
eval(file.readTextSync("lib/ocr.js"));
eval(file.readTextSync("lib/autoPick.js"));
eval(file.readTextSync("lib/exp.js"));
eval(file.readTextSync("lib/backStats.js"));
@@ -851,35 +852,63 @@ function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) {
// 提取所有需要扫描的材料(含怪物材料)
const allMaterials = Object.values(materialCategoryMap).flat();
log.info(`【材料基准】本次需扫描的全量材料:${allMaterials.join("、")}`);
const filteredMaterials = pathingMaterialCounts
.filter(item =>
allMaterials.includes(item.name) &&
(item.count < targetCount || item.count === "?")
)
// ========== 第一步:平行判断超量材料(原始数据,不经过低数量过滤) ==========
pathingMaterialCounts.forEach(item => {
// 只处理allMaterials内的材料(同源)
if (!allMaterials.includes(item.name)) return;
// 未知数量(?)不判断超量
if (item.count === "?") return;
// 矿石数量特殊处理(和低数量筛选的处理逻辑一致)
let processedCount = Number(item.count);
if (specialMaterials.includes(item.name)) {
processedCount = Math.floor(processedCount / 10);
}
// 超量判断(平行逻辑:只要≥阈值就标记,和低数量无关)
if (processedCount >= EXCESS_THRESHOLD) {
tempExcess.push(item.name);
log.debug(`【超量标记】${item.name} 原始数量:${item.count} → 处理后:${processedCount} ≥ 阈值${EXCESS_THRESHOLD},标记为超量`);
}
});
// ========== 第二步:平行筛选低数量材料(原有逻辑保留) ==========
const filteredLowCountMaterials = pathingMaterialCounts
.filter(item => {
// 只处理allMaterials内的材料同源
if (!allMaterials.includes(item.name)) return false;
// 低数量判断:<目标值 或 数量未知(?
return item.count < targetCount || item.count === "?";
})
.map(item => {
// 矿石数量÷10
let processedCount = item.count;
if (specialMaterials.includes(item.name) && item.count !== "?") {
processedCount = Math.floor(Number(item.count) / 10);
}
// 判断是否超量(用处理后数量对比阈值)
if (item.count !== "?" && processedCount >= EXCESS_THRESHOLD) {
tempExcess.push(item.name); // 记录超量材料名
}
return { ...item, count: processedCount };
});
tempExcess.push("OCR启动"); // 添加特殊标记用于终止OCR等待
// 更新全局超量名单(去重)
// ========== 第三步:更新全局超量名单(去重) ==========
excessMaterialNames = [...new Set(tempExcess)];
log.info(`【超量材料更新】共${excessMaterialNames.length}种:${excessMaterialNames.join("、")}`);
log.info(`【低数量材料】筛选后共${filteredLowCountMaterials.length}种:${filteredLowCountMaterials.map(m => m.name).join("、")}`);
return filteredMaterials;
// 返回低数量材料(超量名单已独立生成)
return filteredLowCountMaterials;
}
// 极简封装:用路径和当前目标发通知,然后执行路径
async function runPathAndNotify(pathingFilePath, currentMaterialName) {
const pathName = basename(pathingFilePath); // 取路径名
if (notify) { // 只在需要通知时执行
notification.Send(`当前执行路径:${pathName}\n目标:${currentMaterialName || '未知'}`);
}
return await pathingScript.runFile(pathingFilePath); // 执行路径
}
// ==============================================
// 路径处理(拆分巨型函数)
// ==============================================
@@ -911,7 +940,7 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir)
// 执行路径
const startTime = new Date().toLocaleString();
const initialPosition = genshin.getPositionFromMap();
await pathingScript.runFile(pathingFilePath);
await runPathAndNotify(pathingFilePath, currentMaterialName);
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
@@ -1024,7 +1053,7 @@ async function processMonsterPathEntry(entry, context) {
const startTime = new Date().toLocaleString();
const initialPosition = genshin.getPositionFromMap();
await pathingScript.runFile(pathingFilePath);
await runPathAndNotify(pathingFilePath, currentMaterialName);
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
@@ -1049,7 +1078,7 @@ async function processMonsterPathEntry(entry, context) {
const startTime = new Date().toLocaleString();
const initialPosition = genshin.getPositionFromMap();
await pathingScript.runFile(pathingFilePath);
await runPathAndNotify(pathingFilePath, currentMaterialName);
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
@@ -1171,7 +1200,7 @@ async function processNormalPathEntry(entry, context) {
const startTime = new Date().toLocaleString();
const initialPosition = genshin.getPositionFromMap();
await pathingScript.runFile(pathingFilePath);
await runPathAndNotify(pathingFilePath, currentMaterialName);
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
@@ -1195,7 +1224,7 @@ async function processNormalPathEntry(entry, context) {
const startTime = new Date().toLocaleString();
const initialPosition = genshin.getPositionFromMap();
await pathingScript.runFile(pathingFilePath);
await runPathAndNotify(pathingFilePath, currentMaterialName);
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
@@ -1499,8 +1528,9 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
// 1. 怪物材料筛选(复用全量扫描结果)
log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 基于全量扫描结果筛选有效材料`);
const filteredMonsterMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果
const validMonsterMaterialNames = filteredMonsterMaterials.map(m => m.name);
const filteredMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 仅调用一次!
// 怪物材料复用结果
const validMonsterMaterialNames = filteredMaterials.map(m => m.name);
log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 筛选后有效材料:${validMonsterMaterialNames.join('、')}`);
// 2. 普通材料筛选(同样复用全量扫描结果,无需再次扫描)
@@ -1509,7 +1539,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
return { allPaths: [], pathingMaterialCounts };
}
log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 基于全量扫描结果筛选低数量材料`);
const lowCountMaterialsFiltered = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果
const lowCountMaterialsFiltered = filteredMaterials; // 复用第一次的结果
const flattenedLowCountMaterials = lowCountMaterialsFiltered.flat().sort((a, b) => a.count - b.count);
const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name);
@@ -1656,20 +1686,23 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
const targetTexts = targetTextCategories[categoryName];
allTargetTexts = allTargetTexts.concat(Object.values(targetTexts).flat());
}
// 关键补充等待超量名单生成由filterLowCountMaterials更新
let waitTimes = 0;
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
await sleep(1000); // 每1秒查一次
waitTimes++;
}
// 若收到终止信号直接退出OCR任务不再执行后续逻辑
if (state.cancelRequested) {
log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号已退出`);
return;
}
// 现在过滤才有效确保excessMaterialNames已生成
allTargetTexts = allTargetTexts.filter(name => !excessMaterialNames.includes(name));
log.info(`OCR最终目标文本已过滤超量${allTargetTexts.join('、')}`);
// 关键补充等待超量名单生成由filterLowCountMaterials更新
let waitTimes = 0;
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
await sleep(1000); // 每1秒查一次
waitTimes++;
}
// 若收到终止信号直接退出OCR任务不再执行后续逻辑
if (state.cancelRequested) {
log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号已退出`);
return;
}
// 现在过滤才有效确保excessMaterialNames已生成
allTargetTexts = allTargetTexts.filter(name => !excessMaterialNames.includes(name));
log.info(`超量名单:${excessMaterialNames.join('、')}`);
log.info(`OCR最终目标文本已过滤超量${allTargetTexts.join('、')}`);
await alignAndInteractTarget(allTargetTexts, fDialogueRo, textxRange, texttolerance);
})();

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "背包统计采集系统",
"version": "2.54",
"version": "2.55",
"bgi_version": "0.44.8",
"description": "可统计背包养成道具、部分食物、素材的数量根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势\n+ 1. 自动判断材料CD不需要管材料CD有没有好\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别不会交互路边的npc或是神像可自定义识别名单具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料自动屏蔽更多详细内容查看readme.md",
"saved_files": [