mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-16 03:33:25 +08:00
@@ -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("未能找到正确的目标文本或未成功交互,跳过该目标文本");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,19 @@ const materialPriority = {
|
||||
"武器突破素材": 6,
|
||||
};
|
||||
|
||||
|
||||
// 2. 数字替换映射表(处理OCR识别误差)
|
||||
var numberReplaceMap = {
|
||||
"O": "0", "o": "0", "Q": "0", "0": "0",
|
||||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||||
"E": "3", "e": "3", "3": "3", "三": "3",
|
||||
"A": "4", "a": "4", "4": "4",
|
||||
"S": "5", "s": "5", "5": "5",
|
||||
"G": "6", "b": "6", "6": "6",
|
||||
"T": "7", "t": "7", "7": "7",
|
||||
"B": "8", "θ": "8", "8": "8",
|
||||
"g": "9", "q": "9", "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": "0",
|
||||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||||
"E": "3", "e": "3", "3": "3", "三": "3",
|
||||
"A": "4", "a": "4", "4": "4",
|
||||
"S": "5", "s": "5", "5": "5",
|
||||
"G": "6", "b": "6", "6": "6",
|
||||
"T": "7", "t": "7", "7": "7",
|
||||
"B": "8", "θ": "8", "8": "8",
|
||||
"g": "9", "q": "9", "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("扫描流程结束");
|
||||
|
||||
|
||||
@@ -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": "0",
|
||||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||||
"E": "3", "e": "3", "3": "3", "三": "3",
|
||||
"A": "4", "a": "4", "4": "4",
|
||||
"S": "5", "s": "5", "5": "5",
|
||||
"G": "6", "b": "6", "6": "6",
|
||||
"T": "7", "t": "7", "7": "7",
|
||||
"B": "8", "θ": "8", "8": "8",
|
||||
"g": "9", "q": "9", "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));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
// 读取现有内容,处理文件不存在的情况
|
||||
|
||||
@@ -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;
|
||||
|
||||
122
repo/js/背包材料统计/lib/ocr.js
Normal file
122
repo/js/背包材料统计/lib/ocr.js
Normal 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 // 超时前最后一次有效截图(可用于排查原因)
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user