mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-15 03:23:22 +08:00
诸多优化修复:!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! (#2878)
* 诸多优化修复:!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! v2.61 背包材料翻页机制加速;修复手动终止路径noRecord模式的额外条件判断不生效;全局图片缓存;识别名单、CD文件和弹窗升级多选框UI; !!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! * Add files via upload * Update manifest.json * Add files via upload * Add files via upload * Update settings.json * Add files via upload * fix typo. --------- Co-authored-by: 起个名字好难的喵 <25520958+MisakaAldrich@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 背包材料统计 v2.60
|
||||
# 背包材料统计 v2.61
|
||||
作者:吉吉喵
|
||||
|
||||
<!-- 新增:全局图片样式,控制连续图片同行显示 -->
|
||||
@@ -41,6 +41,8 @@
|
||||
3. **独立名单识别**:不与路边NPC、神像交互;可自定义识别名单(操作见「四、问题解答Q4」);
|
||||
4. **实时弹窗保护**:内置弹窗模块(覆盖路边信件、过期物品、月卡、调查等场景),运行时全程保护路径不被弹窗干扰。
|
||||
5. **自动黑名单**:内置拾取模块,联动材料统计,可识别爆满的路径材料,自动屏蔽。
|
||||
6. **路径检测码**:根据路径生成检测码,可识别改名单坐标未变的路径文件,自动匹配带检测码的路径记录。
|
||||
7. **怪物材料防爆仓**:对怪物材料路径添加多重机制,防止蓝紫怪物材料爆仓(怪物材料路径执行时,不会拾其他的怪物材料,除非pathing文件夹里存在且未超量)。
|
||||
|
||||
|
||||
## 二、用前须知
|
||||
@@ -116,11 +118,11 @@
|
||||
| 4. 发送通知 | ① 每类材料跑完通知一次;② 全部材料跑完汇总通知一次(需开启BGI通知) | 建议开启,方便实时了解进度(接收端如企业微信需自行配置) |
|
||||
| 5. 取消扫描 | 取消“每个路径执行后”的背包扫描,仅保留“全部执行前/后”2次扫描 | 有效路径记录达3条以上时可以开启,可节约运行时间 |
|
||||
| 6. 仅 pathing 材料 | 仅扫描 `pathing` 文件夹内的材料,跳过其他分类,大幅缩短扫描时间 | 路径配置完成后开启,提升脚本运行效率 |
|
||||
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) |
|
||||
| 8. 采用的 CD 分类 | 不填则默认执行 `materialsCD` 文件夹内配置的CD分类;填写则仅执行指定CD分类 | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答Q2」) |
|
||||
| 7. 弹窗名 | 不选则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) |
|
||||
| 8. 采用的 CD 分类 | 不选则默认执行 `materialsCD` 文件夹内配置的CD分类;填写则仅执行指定CD分类 | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答Q2」) |
|
||||
| 9. 终止时刻 | 不填则不执行定时终止;路径无时间记录时,会预判路径耗时5分钟,且预留2分钟空闲 | 填写需要按24小时格式(例:4:10) |
|
||||
| 10. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时,需符合配置规则(操作见「四、问题解答Q4」) |
|
||||
| 11. 超量阈值 | 首次扫描后,超量的路径材料,将从识别名单中剔除,默认5000 | 不推荐9999,怪物材料有几千就够了,采用默认数值,可自动避免爆背包 |
|
||||
| 10. 采用的识别名单 | 不选则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时,需符合配置规则(操作见「四、问题解答Q4」) |
|
||||
| 11. 超量阈值 | 首次扫描后,超量的路径材料,将从识别名单中剔除,默认9000 | 不推荐9999,怪物材料有几千就够了,采用默认数值,可自动避免爆背包 |
|
||||
| 12. 拖动距离 | 解决非1080p分辨率下“划页过头”问题,需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”;大于1080p屏可适当减小数值 |
|
||||
|
||||
|
||||
@@ -130,7 +132,7 @@
|
||||
3. **食物识别强制要求**:背包食物界面**第一行必须包含8种食物**(苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪),缺少则这些食物无法识别;
|
||||
4. **关键文件备份**:建议不定期备份 `pathing` 文件夹(路径文件)和 `pathing_record` 文件夹(路径运行记录),便于丢失后或被污染后,记录能恢复如初;
|
||||
5. **OCR配置**:默认最新,调整识别名单时,用的是V5Auto;
|
||||
6. **手动终止运行**:如果要终止JS运行,推荐在当前路径采集到当前材料前,或者采集完进入背包扫描时终止(会在扫描结束后终止),以保护当前记录;如果是【取消扫描】模式,不会储存当前记录的材料数目,就随意。
|
||||
6. **手动终止运行**:手动终止路径会被记录成noRecord模式,只参与CD计算,如果是【取消扫描】模式,不会储存当前记录的材料数目,两种情况都随意。
|
||||
|
||||
|
||||
## 五、问题解答
|
||||
@@ -230,4 +232,5 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
|
||||
| v2.57 | 补全圣遗物无CD管理bug |
|
||||
| v2.58 | 优化背包扫图逻辑 |
|
||||
| v2.59 | 修复自动拾取匹配bug,改为双向匹配 |
|
||||
| v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错 |
|
||||
| v2.60 | 手动终止路径会被记录成noRecord模式,只参与CD计算;增加当前路线预估时长日志;材料分类升级多选框UI,刚需bgi v0.55版本;优化文件是否存在逻辑;降级ReadTextSync报错;检测码识别路径 |
|
||||
| v2.61 | 背包材料识别机制加速;修复手动终止路径noRecord模式的额外条件判断不生效;全局图片缓存;识别名单、CD文件和弹窗升级多选框UI!!!!!!注意UI修改了,配置组里需要删除并重新添加该js!!!!!! |
|
||||
@@ -26,14 +26,34 @@ function parseCategoryContent(content) {
|
||||
|
||||
// 从 targetText 文件夹中读取分类信息(无修改,与OCR无关)
|
||||
function readtargetTextCategories(targetTextDir) {
|
||||
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1);
|
||||
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1, ['.txt']);
|
||||
const materialCategories = {};
|
||||
|
||||
// 解析筛选名单
|
||||
const pickTextNames = (settings.PickCategories || "")
|
||||
.split(/[,,、 \s]+/).map(n => n.trim()).filter(n => n);
|
||||
let pickTextNames = [];
|
||||
try {
|
||||
pickTextNames = Array.from(settings.PickCategories || []);
|
||||
} catch (e) {
|
||||
log.error(`获取PickCategories设置失败: ${e.message}`);
|
||||
}
|
||||
|
||||
let availablePickCategories = [];
|
||||
try {
|
||||
availablePickCategories = targetTextFilePaths.map(filePath => basename(filePath).replace('.txt', ''));
|
||||
log.info(`可用识别名单:${availablePickCategories.join(', ')}`);
|
||||
} catch (e) {
|
||||
log.error(`扫描识别名单目录失败: ${e.message}`);
|
||||
}
|
||||
|
||||
if (pickTextNames.length === 0) {
|
||||
log.info("未指定识别名单,将加载所有文件");
|
||||
} else {
|
||||
const invalidCategories = pickTextNames.filter(name => !availablePickCategories.includes(name));
|
||||
if (invalidCategories.length > 0) {
|
||||
log.warn(`以下识别名单不存在,将被忽略:${invalidCategories.join(', ')}`);
|
||||
pickTextNames = pickTextNames.filter(name => availablePickCategories.includes(name));
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底日志:确认pickTextNames是否为空,方便排查
|
||||
log.info(`筛选名单状态:${pickTextNames.length === 0 ? '未指定(空),将加载所有文件' : '指定了:' + pickTextNames.join(',')}`);
|
||||
|
||||
for (const filePath of targetTextFilePaths) {
|
||||
@@ -92,36 +112,38 @@ const ScrollRo = RecognitionObject.TemplateMatch(
|
||||
|
||||
/**
|
||||
* 对齐并交互目标(核心改造:适配最新版performOcr)
|
||||
* @param {string[]} targetTexts - 待匹配的目标文本列表
|
||||
* @param {string[]|Function} targetTextsOrFunc - 待匹配的目标文本列表或函数
|
||||
* @param {Object} fDialogueRo - F图标的识别对象
|
||||
* @param {Object} textxRange - 文本识别的X轴范围 { min: number, max: number }
|
||||
* @param {number} texttolerance - 文本与F图标Y轴对齐的容差
|
||||
* @param {Object} cachedFrame - 缓存的图像帧(可选)
|
||||
*/
|
||||
async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame = null) {
|
||||
async function alignAndInteractTarget(targetTextsOrFunc, fDialogueRo, textxRange, texttolerance, cachedFrame = null) {
|
||||
let lastLogTime = Date.now();
|
||||
const recognitionCount = new Map(); // 避免误触:文本+Y坐标 → 计数
|
||||
const ocrScreenshots = []; // 收集最新版performOcr返回的截图,统一释放
|
||||
const recognitionCount = new Map();
|
||||
const ocrScreenshots = [];
|
||||
|
||||
try {
|
||||
while (!state.completed && !state.cancelRequested) {
|
||||
recognitionCount.clear(); // 每次循环开始时清空计数
|
||||
if (state.ocrPaused) {
|
||||
await sleep(100);
|
||||
continue;
|
||||
}
|
||||
recognitionCount.clear();
|
||||
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();
|
||||
|
||||
// 2. 识别F图标/Scroll.png(保留原逻辑)
|
||||
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
|
||||
if (!fRes) {
|
||||
const scrollRes = await findFIcon(ScrollRo, 10, cachedFrame);
|
||||
@@ -132,21 +154,19 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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); // 收集截图,避免内存泄漏
|
||||
const targetTexts = typeof targetTextsOrFunc === 'function' ? targetTextsOrFunc() : targetTextsOrFunc;
|
||||
|
||||
const yRange = { min: fRes.y - 3, max: fRes.y + 37 };
|
||||
const { results: ocrResults, screenshot: ocrScreenshot } = await performOcr(
|
||||
targetTexts,
|
||||
textxRange,
|
||||
yRange,
|
||||
cachedFrame,
|
||||
10,
|
||||
5
|
||||
);
|
||||
ocrScreenshots.push(ocrScreenshot);
|
||||
|
||||
// 4. 文本匹配与交互(双向匹配,增强容错性)
|
||||
let foundTarget = false;
|
||||
for (const targetText of targetTexts) {
|
||||
const targetResult = ocrResults.find(res =>
|
||||
@@ -154,11 +174,9 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
);
|
||||
if (!targetResult) continue;
|
||||
|
||||
// 计数防误触
|
||||
const materialId = `${targetText}-${targetResult.y}`;
|
||||
recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1);
|
||||
|
||||
// Y轴对齐判断
|
||||
const centerYTargetText = targetResult.y + targetResult.height / 2;
|
||||
if (Math.abs(centerYTargetText - (fRes.y + fRes.height / 2)) <= texttolerance) {
|
||||
if (recognitionCount.get(materialId) >= 1) {
|
||||
@@ -171,7 +189,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 未找到目标则翻滚(保留原逻辑)
|
||||
if (!foundTarget) {
|
||||
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
|
||||
}
|
||||
@@ -179,20 +196,16 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
} 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) {
|
||||
log.info("检测任务已取消");
|
||||
} else if (!state.completed) {
|
||||
|
||||
@@ -5,6 +5,9 @@ const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 75
|
||||
const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711));
|
||||
const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay);
|
||||
|
||||
// 全局图片缓存(避免重复加载)
|
||||
const globalMaterialImageCache = {};
|
||||
|
||||
// 配置参数
|
||||
const pageScrollCount = 22; // 最多滑页次数
|
||||
|
||||
@@ -127,7 +130,22 @@ async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) {
|
||||
stepDistance,
|
||||
stepInterval: delayMs,
|
||||
waitBefore: 50,
|
||||
waitAfter: 700, // 原逻辑中松开后等待700ms
|
||||
waitAfter: 500, // 原逻辑中松开后等待700ms
|
||||
repeat: 1
|
||||
});
|
||||
}
|
||||
|
||||
// 回退翻页函数(用于后续优先级材料扫描)
|
||||
async function scrollBackPage(totalDistance, stepDistance = 10, delayMs = 5) {
|
||||
const backHoldY = 1080 - holdY; // 回退翻页的Y值
|
||||
await mouseDrag({
|
||||
holdMouseX: holdX, // 固定起点X
|
||||
holdMouseY: backHoldY, // 回退翻页的Y值
|
||||
totalDistance: totalDistance, // 向下滑动(正值)
|
||||
stepDistance,
|
||||
stepInterval: delayMs,
|
||||
waitBefore: 50,
|
||||
waitAfter: 500,
|
||||
repeat: 1
|
||||
});
|
||||
}
|
||||
@@ -281,49 +299,47 @@ async function mouseDrag({
|
||||
}
|
||||
|
||||
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 backPriorityMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType);
|
||||
const allSameTypeMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialTypeMap[mat] === currentType);
|
||||
|
||||
// 合并当前和后位材料分类(只包含同位和后位,不包含前位)
|
||||
// 只有同位或后位材料才会触发全列扫描
|
||||
const finalFilteredMaterials = [...backPriorityMaterials, materialsCategory];
|
||||
const finalFilteredMaterials = allSameTypeMaterials.sort((a, b) => materialPriority[a] - materialPriority[b]);
|
||||
return finalFilteredMaterials
|
||||
}
|
||||
|
||||
// 扫描材料
|
||||
async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
// 材料图片缓存
|
||||
const materialImages = {}; // 用于缓存加载的图片
|
||||
const priorityMaterialImages = {}; // 用于缓存优先级材料图片
|
||||
async function scanMaterials(materialsCategory, materialCategoryMap, isPostPriority = false) {
|
||||
// 使用全局图片缓存
|
||||
const materialImages = globalMaterialImageCache;
|
||||
|
||||
const currentType = materialTypeMap[materialsCategory];
|
||||
const currentPriority = materialPriority[materialsCategory];
|
||||
|
||||
// 获取当前+后位材料名单(仅包含同位和后位,不包含前位)
|
||||
const priorityMaterialNames = [];
|
||||
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
|
||||
for (const category of finalFilteredMaterials) {
|
||||
|
||||
const currentTypeMaterials = finalFilteredMaterials.filter(category => materialTypeMap[category] === currentType);
|
||||
|
||||
for (const category of currentTypeMaterials) {
|
||||
const materialIconDir = `assets/images/${category}`;
|
||||
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
|
||||
for (const filePath of materialIconFilePaths) {
|
||||
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
|
||||
const name = basename(filePath).replace(".png", "");
|
||||
priorityMaterialNames.push({ category, name });
|
||||
|
||||
// 预加载优先级材料图片
|
||||
if (!priorityMaterialImages[name]) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (!materialImages[name]) {
|
||||
const mat = getCachedImageMat(filePath);
|
||||
if (!mat.empty()) {
|
||||
priorityMaterialImages[name] = mat;
|
||||
materialImages[name] = mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +368,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
const mat = getCachedImageMat(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图标失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
@@ -413,33 +429,119 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性
|
||||
let phrasesStartTime = Date.now();
|
||||
let previousScreenshot = null; // 用于存储上一次翻页前的截图
|
||||
|
||||
// 后续优先级材料扫描:滑条重置后先检查第八列是否有前位材料
|
||||
if (isPostPriority) {
|
||||
log.info(`后续优先级材料扫描 - 检查第八列前位材料`);
|
||||
const ra = captureGameRegion();
|
||||
|
||||
const lowerPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] < currentPriority);
|
||||
log.info(`检查前位材料分类: ${lowerPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`);
|
||||
const lowerPriorityMatches = [];
|
||||
|
||||
for (const category of lowerPriorityMaterials) {
|
||||
const categoryMaterials = priorityMaterialNames
|
||||
.filter(({ name, category: cat }) => cat === category)
|
||||
.map(({ name }) => {
|
||||
const mat = materialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (categoryMaterials.length > 0) {
|
||||
const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8);
|
||||
const foundMaterials = matchResults.filter(r => r.result).map(r => r.name);
|
||||
if (foundMaterials.length > 0) {
|
||||
log.info(`第八列识别到前位材料 [${category}]: ${foundMaterials.join(', ')}`);
|
||||
}
|
||||
lowerPriorityMatches.push(...matchResults.filter(r => r.result));
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`第八列前位材料总数: ${lowerPriorityMatches.length}`);
|
||||
if (lowerPriorityMatches.length === 0) {
|
||||
log.info(`未发现前位材料,回退一页`);
|
||||
await scrollBackPage(totalPageDistance, 10, 5);
|
||||
await sleep(500);
|
||||
} else {
|
||||
log.info(`发现前位材料,照常继续扫描`);
|
||||
}
|
||||
}
|
||||
|
||||
// 扫描背包中的材料
|
||||
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
|
||||
|
||||
const ra = captureGameRegion();
|
||||
// 重置foundPriorityMaterial标志,每次翻页都重新检查
|
||||
foundPriorityMaterial = false;
|
||||
|
||||
// 检查第八列是否有目标材料
|
||||
// priorityMaterialNames只包含当前和后位材料
|
||||
const priorityMaterialsToMatch = priorityMaterialNames
|
||||
.filter(({ name }) => !recognizedMaterials.has(name))
|
||||
.map(({ name }) => {
|
||||
const mat = priorityMaterialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
|
||||
|
||||
if (priorityMaterialsToMatch.length > 0) {
|
||||
const matchResults = await parallelTemplateMatch(ra, priorityMaterialsToMatch, 1142, startY, columnWidth, columnHeight, 0.8);
|
||||
const currentTypeMaterials = finalFilteredMaterials.filter(category => materialTypeMap[category] === currentType);
|
||||
|
||||
for (const { name, result } of matchResults) {
|
||||
if (result) {
|
||||
foundPriorityMaterial = true; // 标记找到目标材料
|
||||
// log.info(`发现目标材料: ${name},开始全列扫描`);
|
||||
break;
|
||||
log.info(`第八列扫描 - 当前分类: ${materialsCategory}, 优先级: ${currentPriority}`);
|
||||
|
||||
if (currentPriority <= 2) {
|
||||
const lowerPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] < currentPriority);
|
||||
log.info(`检查前位材料分类: ${lowerPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`);
|
||||
const lowerPriorityMatches = [];
|
||||
|
||||
for (const category of lowerPriorityMaterials) {
|
||||
const categoryMaterials = priorityMaterialNames
|
||||
.filter(({ name, category: cat }) => cat === category && !recognizedMaterials.has(name))
|
||||
.map(({ name }) => {
|
||||
const mat = materialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (categoryMaterials.length > 0) {
|
||||
const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8);
|
||||
const foundMaterials = matchResults.filter(r => r.result).map(r => r.name);
|
||||
if (foundMaterials.length > 0) {
|
||||
log.info(`第八列识别到前位材料 [${category}]: ${foundMaterials.join(', ')}`);
|
||||
}
|
||||
lowerPriorityMatches.push(...matchResults.filter(r => r.result));
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`第八列前位材料总数: ${lowerPriorityMatches.length}`);
|
||||
if (lowerPriorityMatches.length < 4) {
|
||||
log.info(`前位材料少于4张,触发全列扫描`);
|
||||
foundPriorityMaterial = true;
|
||||
} else {
|
||||
log.info(`4张都是前位材料,继续翻页`);
|
||||
}
|
||||
} else {
|
||||
const currentOrHigherPriorityMaterials = currentTypeMaterials.filter(category => materialPriority[category] >= currentPriority);
|
||||
log.info(`检查同位/后位材料分类: ${currentOrHigherPriorityMaterials.map(c => `${c}(优先级${materialPriority[c]})`).join(', ')}`);
|
||||
const currentOrHigherMatches = [];
|
||||
|
||||
for (const category of currentOrHigherPriorityMaterials) {
|
||||
const categoryMaterials = priorityMaterialNames
|
||||
.filter(({ name, category: cat }) => cat === category && !recognizedMaterials.has(name))
|
||||
.map(({ name }) => {
|
||||
const mat = materialImages[name];
|
||||
return mat ? { name, mat } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (categoryMaterials.length > 0) {
|
||||
const matchResults = await parallelTemplateMatch(ra, categoryMaterials, 1142, startY, columnWidth, columnHeight, 0.8);
|
||||
const foundMaterials = matchResults.filter(r => r.result).map(r => r.name);
|
||||
if (foundMaterials.length > 0) {
|
||||
log.info(`第八列识别到同位/后位材料 [${category}]: ${foundMaterials.join(', ')}`);
|
||||
}
|
||||
currentOrHigherMatches.push(...matchResults.filter(r => r.result));
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`第八列同位/后位材料总数: ${currentOrHigherMatches.length}`);
|
||||
if (currentOrHigherMatches.length > 0) {
|
||||
log.info(`发现同位/后位材料,触发全列扫描`);
|
||||
foundPriorityMaterial = true;
|
||||
} else {
|
||||
log.info(`未发现同位/后位材料,继续翻页`);
|
||||
}
|
||||
}
|
||||
|
||||
// 只有发现目标材料时,才执行全列扫描
|
||||
@@ -558,7 +660,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
}
|
||||
|
||||
// 滑动到下一页
|
||||
if (scroll < pageScrollCount) {
|
||||
if (scroll < pageScrollCount && !shouldEndScan) {
|
||||
if (useScreenComparison && previousScreenshot) {
|
||||
// 使用模板匹配比较两次翻页前的截图(兜底机制)
|
||||
const matchRo = RecognitionObject.TemplateMatch(
|
||||
@@ -733,219 +835,232 @@ function dynamicMaterialGrouping(materialCategoryMap) {
|
||||
|
||||
// 主逻辑函数
|
||||
async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
|
||||
// 1. 先记录原始名称与别名的映射关系(用于最后反向转换)
|
||||
const nameMap = new Map();
|
||||
Object.values(materialCategoryMap).flat().forEach(originalName => {
|
||||
const aliasName = MATERIAL_ALIAS[originalName] || originalName;
|
||||
nameMap.set(aliasName, originalName); // 存储:别名→原始名
|
||||
});
|
||||
|
||||
// 2. 转换materialCategoryMap为别名(用于内部处理)
|
||||
const processedMap = {};
|
||||
Object.entries(materialCategoryMap).forEach(([category, names]) => {
|
||||
processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name);
|
||||
});
|
||||
materialCategoryMap = processedMap;
|
||||
|
||||
const maxStage = 4; // 最大阶段数
|
||||
let stage = 0; // 当前阶段
|
||||
let currentGroupIndex = 0; // 当前处理的分组索引
|
||||
let currentCategoryIndex = 0; // 当前处理的分类索引
|
||||
let materialsCategory = ""; // 当前处理的材料分类名称
|
||||
const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息
|
||||
|
||||
// 添加状态变量,记录上一个分类的信息
|
||||
let prevCategory = null;
|
||||
let prevCategoryObject = null;
|
||||
let prevPriority = null;
|
||||
let prevGroup = null;
|
||||
let skipSliderReset = false; // 是否跳过滑条重置
|
||||
|
||||
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
|
||||
// log.info("材料 动态[分组]结果:");
|
||||
sortedGroups.forEach(group => {
|
||||
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
|
||||
});
|
||||
|
||||
let loopCount = 0;
|
||||
const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环
|
||||
|
||||
while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 =====
|
||||
loopCount++;
|
||||
switch (stage) {
|
||||
case 0: // 返回主界面
|
||||
log.info("返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
await sleep(500);
|
||||
stage = 1; // 进入下一阶段
|
||||
break;
|
||||
|
||||
case 1: // 打开背包界面
|
||||
// log.info("打开背包界面");
|
||||
keyPress("B"); // 打开背包界面
|
||||
await sleep(800); // 减少等待时间
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000);
|
||||
if (backpackResult.isDetected) {
|
||||
// log.info("成功识别背包图标");
|
||||
stage = 2; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到背包图标,重新尝试");
|
||||
// ===== 补充优化:连续回退时释放资源 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // 按分组处理材料分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
materialsCategory = group.categories[currentCategoryIndex];
|
||||
const offset = materialTypeMap[materialsCategory];
|
||||
const menuClickX = Math.round(575 + (offset - 1) * 96.25);
|
||||
// log.info(`点击坐标 (${menuClickX},75)`);
|
||||
click(menuClickX, 75);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
stage = 3; // 进入下一阶段
|
||||
} else {
|
||||
currentGroupIndex++;
|
||||
currentCategoryIndex = 0; // 重置分类索引
|
||||
stage = 2; // 继续处理下一组
|
||||
}
|
||||
} else {
|
||||
stage = 5; // 跳出循环
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // 识别材料分类
|
||||
let CategoryObject = getCategoryObject(materialsCategory);
|
||||
if (!CategoryObject) {
|
||||
log.error("未知的材料分类");
|
||||
// ===== 补充优化:异常时释放资源并退出 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退到阶段0
|
||||
return;
|
||||
}
|
||||
|
||||
const CategoryResult = await recognizeImage(CategoryObject, cachedFrame);
|
||||
if (CategoryResult.isDetected) {
|
||||
log.info(`识别到${materialsCategory} 所在分类。`);
|
||||
stage = 4; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到材料分类图标,重新尝试");
|
||||
// log.warn(`识别结果:${JSON.stringify(CategoryResult)}`);
|
||||
// ===== 补充优化:连续回退时释放资源 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 2; // 回退到阶段2
|
||||
}
|
||||
break;
|
||||
|
||||
case 4: // 扫描材料
|
||||
log.info("芭芭拉,冲鸭!");
|
||||
|
||||
// 判断是否需要重置滑条
|
||||
if (!skipSliderReset) {
|
||||
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
|
||||
await sleep(200);
|
||||
leftButtonDown(); // 长按左键重置材料滑条
|
||||
await sleep(300);
|
||||
leftButtonUp();
|
||||
await sleep(200);
|
||||
} else {
|
||||
log.info("同一大类且为后位材料,跳过滑条重置");
|
||||
// 不重置滑条,直接从当前位置开始检查第八列
|
||||
}
|
||||
|
||||
// 扫描材料并获取低于目标数量的材料
|
||||
const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap);
|
||||
allLowCountMaterials.push(lowCountMaterials);
|
||||
|
||||
// 保存当前分类信息,用于下一个分类的判断
|
||||
prevCategory = materialsCategory;
|
||||
prevPriority = materialPriority[materialsCategory];
|
||||
|
||||
// 获取当前分类的CategoryObject
|
||||
const currentCategoryObject = getCategoryObject(materialsCategory);
|
||||
prevCategoryObject = currentCategoryObject;
|
||||
prevGroup = sortedGroups[currentGroupIndex];
|
||||
|
||||
currentCategoryIndex++;
|
||||
|
||||
// 判断下一个分类是否是同一个大类CategoryObject下的后位材料
|
||||
let nextCategory = null;
|
||||
let nextCategoryObject = null;
|
||||
let nextPriority = null;
|
||||
|
||||
// 检查是否还有下一个分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
nextCategory = group.categories[currentCategoryIndex];
|
||||
|
||||
// 获取下一个分类的CategoryObject
|
||||
nextCategoryObject = getCategoryObject(nextCategory);
|
||||
|
||||
// 获取下一个分类的优先级
|
||||
nextPriority = materialPriority[nextCategory];
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否跳过滑条重置:同一大类且为后位材料
|
||||
if (nextCategory &&
|
||||
nextCategoryObject === prevCategoryObject &&
|
||||
nextPriority > prevPriority) {
|
||||
skipSliderReset = true;
|
||||
} else {
|
||||
skipSliderReset = false;
|
||||
}
|
||||
|
||||
stage = 2; // 返回阶段2处理下一个分类
|
||||
break;
|
||||
|
||||
case 5: // 所有分组处理完毕
|
||||
log.info("所有分组处理完毕,返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
stage = maxStage + 1; // 确保退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 补充优化:循环超限处理,防止卡死 =====
|
||||
if (loopCount > maxLoopCount) {
|
||||
log.error(`主循环次数超限(${maxLoopCount}次),强制退出`);
|
||||
cachedFrame?.dispose();
|
||||
await genshin.returnMainUi();
|
||||
return [];
|
||||
}
|
||||
|
||||
await genshin.returnMainUi(); // 返回主界面
|
||||
log.info("扫描流程结束");
|
||||
|
||||
|
||||
// 3. 处理完成后,将输出结果转换回原始名称
|
||||
const finalResult = allLowCountMaterials.map(categoryMaterials => {
|
||||
return categoryMaterials.map(material => {
|
||||
// 假设material包含name属性,将别名转回原始名
|
||||
return {
|
||||
...material,
|
||||
name: nameMap.get(material.name) || material.name // 反向映射
|
||||
};
|
||||
try {
|
||||
// 1. 先记录原始名称与别名的映射关系(用于最后反向转换)
|
||||
const nameMap = new Map();
|
||||
Object.values(materialCategoryMap).flat().forEach(originalName => {
|
||||
const aliasName = MATERIAL_ALIAS[originalName] || originalName;
|
||||
nameMap.set(aliasName, originalName); // 存储:别名→原始名
|
||||
});
|
||||
});
|
||||
|
||||
cachedFrame?.dispose();
|
||||
return finalResult; // 返回转换后的结果(如"晶蝶")
|
||||
// 2. 转换materialCategoryMap为别名(用于内部处理)
|
||||
const processedMap = {};
|
||||
Object.entries(materialCategoryMap).forEach(([category, names]) => {
|
||||
processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name);
|
||||
});
|
||||
materialCategoryMap = processedMap;
|
||||
|
||||
const maxStage = 4; // 最大阶段数
|
||||
let stage = 0; // 当前阶段
|
||||
let currentGroupIndex = 0; // 当前处理的分组索引
|
||||
let currentCategoryIndex = 0; // 当前处理的分类索引
|
||||
let materialsCategory = ""; // 当前处理的材料分类名称
|
||||
const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息
|
||||
|
||||
// 添加状态变量,记录上一个分类的信息
|
||||
let prevCategory = null;
|
||||
let prevCategoryObject = null;
|
||||
let prevPriority = null;
|
||||
let prevGroup = null;
|
||||
let skipSliderReset = false; // 是否跳过滑条重置
|
||||
|
||||
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
|
||||
// log.info("材料 动态[分组]结果:");
|
||||
sortedGroups.forEach(group => {
|
||||
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
|
||||
});
|
||||
|
||||
let loopCount = 0;
|
||||
const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环
|
||||
|
||||
while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 =====
|
||||
loopCount++;
|
||||
switch (stage) {
|
||||
case 0: // 返回主界面
|
||||
log.info("返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
await sleep(500);
|
||||
stage = 1; // 进入下一阶段
|
||||
break;
|
||||
|
||||
case 1: // 打开背包界面
|
||||
// log.info("打开背包界面");
|
||||
keyPress("B"); // 打开背包界面
|
||||
state.ocrPaused = true;
|
||||
log.info("背包扫描开始,已暂停OCR拾取任务");
|
||||
await sleep(800); // 减少等待时间
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000, 500, true, "背包");
|
||||
if (backpackResult.isDetected) {
|
||||
// log.info("成功识别背包图标");
|
||||
stage = 2; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到背包图标,重新尝试");
|
||||
// ===== 补充优化:连续回退时释放资源 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // 按分组处理材料分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
materialsCategory = group.categories[currentCategoryIndex];
|
||||
const offset = materialTypeMap[materialsCategory];
|
||||
const menuClickX = Math.round(575 + (offset - 1) * 96.25);
|
||||
// log.info(`点击坐标 (${menuClickX},75)`);
|
||||
click(menuClickX, 75);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
stage = 3; // 进入下一阶段
|
||||
} else {
|
||||
currentGroupIndex++;
|
||||
currentCategoryIndex = 0; // 重置分类索引
|
||||
stage = 2; // 继续处理下一组
|
||||
}
|
||||
} else {
|
||||
stage = 5; // 跳出循环
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // 识别材料分类
|
||||
let CategoryObject = getCategoryObject(materialsCategory);
|
||||
if (!CategoryObject) {
|
||||
log.error("未知的材料分类");
|
||||
// ===== 补充优化:异常时释放资源并退出 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 0; // 回退到阶段0
|
||||
return;
|
||||
}
|
||||
|
||||
const CategoryResult = await recognizeImage(CategoryObject, cachedFrame, 2000, 500, true, materialsCategory);
|
||||
if (CategoryResult.isDetected) {
|
||||
log.info(`识别到${materialsCategory} 所在分类。`);
|
||||
stage = 4; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到材料分类图标,重新尝试");
|
||||
// log.warn(`识别结果:${JSON.stringify(CategoryResult)}`);
|
||||
// ===== 补充优化:连续回退时释放资源 =====
|
||||
cachedFrame?.dispose();
|
||||
stage = 2; // 回退到阶段2
|
||||
}
|
||||
break;
|
||||
|
||||
case 4: // 扫描材料
|
||||
log.info("芭芭拉,冲鸭!");
|
||||
|
||||
// 判断是否需要重置滑条
|
||||
if (!skipSliderReset) {
|
||||
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
|
||||
await sleep(200);
|
||||
leftButtonDown(); // 长按左键重置材料滑条
|
||||
await sleep(300);
|
||||
leftButtonUp();
|
||||
await sleep(200);
|
||||
} else {
|
||||
log.info("同一大类且为后位材料,跳过滑条重置");
|
||||
// 不重置滑条,直接从当前位置开始检查第八列
|
||||
}
|
||||
|
||||
// 判断是否是后续优先级材料(优先级高于前一个分类)
|
||||
const currentPriority = materialPriority[materialsCategory];
|
||||
const isPostPriority = prevPriority !== null && currentPriority > prevPriority;
|
||||
if (isPostPriority) {
|
||||
log.info(`后续优先级材料扫描 - 当前优先级: ${currentPriority}, 前位优先级: ${prevPriority}`);
|
||||
}
|
||||
|
||||
// 扫描材料并获取低于目标数量的材料
|
||||
const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap, isPostPriority);
|
||||
allLowCountMaterials.push(lowCountMaterials);
|
||||
|
||||
// 保存当前分类信息,用于下一个分类的判断
|
||||
prevCategory = materialsCategory;
|
||||
prevPriority = materialPriority[materialsCategory];
|
||||
|
||||
// 获取当前分类的CategoryObject
|
||||
const currentCategoryObject = getCategoryObject(materialsCategory);
|
||||
prevCategoryObject = currentCategoryObject;
|
||||
prevGroup = sortedGroups[currentGroupIndex];
|
||||
|
||||
currentCategoryIndex++;
|
||||
|
||||
// 判断下一个分类是否是同一个大类CategoryObject下的后位材料
|
||||
let nextCategory = null;
|
||||
let nextCategoryObject = null;
|
||||
let nextPriority = null;
|
||||
|
||||
// 检查是否还有下一个分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
nextCategory = group.categories[currentCategoryIndex];
|
||||
|
||||
// 获取下一个分类的CategoryObject
|
||||
nextCategoryObject = getCategoryObject(nextCategory);
|
||||
|
||||
// 获取下一个分类的优先级
|
||||
nextPriority = materialPriority[nextCategory];
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否跳过滑条重置:同一大类且为后位材料
|
||||
if (nextCategory &&
|
||||
nextCategoryObject === prevCategoryObject &&
|
||||
nextPriority > prevPriority) {
|
||||
skipSliderReset = true;
|
||||
} else {
|
||||
skipSliderReset = false;
|
||||
}
|
||||
|
||||
stage = 2; // 返回阶段2处理下一个分类
|
||||
break;
|
||||
|
||||
case 5: // 所有分组处理完毕
|
||||
log.info("所有分组处理完毕,返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
stage = maxStage + 1; // 确保退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 补充优化:循环超限处理,防止卡死 =====
|
||||
if (loopCount > maxLoopCount) {
|
||||
log.error(`主循环次数超限(${maxLoopCount}次),强制退出`);
|
||||
cachedFrame?.dispose();
|
||||
await genshin.returnMainUi();
|
||||
return [];
|
||||
}
|
||||
|
||||
await genshin.returnMainUi(); // 返回主界面
|
||||
log.info("扫描流程结束");
|
||||
|
||||
|
||||
// 3. 处理完成后,将输出结果转换回原始名称
|
||||
const finalResult = allLowCountMaterials.map(categoryMaterials => {
|
||||
return categoryMaterials.map(material => {
|
||||
// 假设material包含name属性,将别名转回原始名
|
||||
return {
|
||||
...material,
|
||||
name: nameMap.get(material.name) || material.name // 反向映射
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
cachedFrame?.dispose();
|
||||
return finalResult; // 返回转换后的结果(如"晶蝶")
|
||||
} finally {
|
||||
state.ocrPaused = false;
|
||||
log.info("背包扫描结束,已恢复OCR拾取任务");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -217,11 +217,36 @@ async function imageClickBackgroundTask() {
|
||||
|
||||
// 配置参数
|
||||
const taskDelay = Math.min(999, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 15))) * 1000;
|
||||
const specificNamesStr = settings.PopupNames || "";
|
||||
const specificNames = specificNamesStr
|
||||
.split(/[,,、 \s]+/)
|
||||
.map(name => name.trim())
|
||||
.filter(name => name !== "");
|
||||
let specificNames = [];
|
||||
try {
|
||||
specificNames = Array.from(settings.PopupNames || []);
|
||||
} catch (e) {
|
||||
log.error(`获取PopupNames设置失败: ${e.message}`);
|
||||
}
|
||||
|
||||
let availablePopupDirs = [];
|
||||
try {
|
||||
const imageClickDir = "assets/imageClick";
|
||||
const subDirs = readAllFilePaths(imageClickDir, 0, 2, [], true);
|
||||
availablePopupDirs = subDirs.filter(subDir => {
|
||||
const dirName = basename(subDir);
|
||||
const entries = readAllFilePaths(subDir, 0, 0, [], true);
|
||||
return entries.some(entry => normalizePath(entry).endsWith('/icon'));
|
||||
}).map(dir => basename(dir));
|
||||
log.info(`可用弹窗目录:${availablePopupDirs.join(', ')}`);
|
||||
} catch (e) {
|
||||
log.error(`扫描弹窗目录失败: ${e.message}`);
|
||||
}
|
||||
|
||||
if (specificNames.length === 0) {
|
||||
log.info("未指定弹窗名称,将处理所有可用弹窗");
|
||||
} else {
|
||||
const invalidNames = specificNames.filter(name => !availablePopupDirs.includes(name));
|
||||
if (invalidNames.length > 0) {
|
||||
log.warn(`以下弹窗名称不存在,将被忽略:${invalidNames.join(', ')}`);
|
||||
specificNames = specificNames.filter(name => availablePopupDirs.includes(name));
|
||||
}
|
||||
}
|
||||
|
||||
// 预加载资源
|
||||
const preloadedResources = await preloadImageResources(specificNames);
|
||||
|
||||
180
repo/js/背包材料统计/lib/updateSettings.js
Normal file
180
repo/js/背包材料统计/lib/updateSettings.js
Normal file
@@ -0,0 +1,180 @@
|
||||
// 自动更新settings.json中的options数组
|
||||
// 使用BetterGI的file对象,不需要Node.js的fs模块
|
||||
|
||||
var SETTINGS_FILE = "settings.json";
|
||||
|
||||
function updateSettingsOptions() {
|
||||
log.info("开始更新settings.json...");
|
||||
try {
|
||||
var settingsContent = file.readTextSync(SETTINGS_FILE);
|
||||
log.info("settings.json内容长度: " + settingsContent.length);
|
||||
var settings = JSON.parse(settingsContent);
|
||||
log.info("settings.json解析成功,配置项数量: " + settings.length);
|
||||
|
||||
var hasChanges = false;
|
||||
|
||||
var popupDirs = readAllFilePaths("assets/imageClick", 0, 2, [], true)
|
||||
.filter(function (dirPath) {
|
||||
var entries = readAllFilePaths(dirPath, 0, 0, [], true);
|
||||
return entries.some(function (entry) {
|
||||
return normalizePath(entry).endsWith('/icon');
|
||||
});
|
||||
})
|
||||
.filter(function (dirPath) {
|
||||
return !normalizePath(dirPath).includes('/其他/');
|
||||
})
|
||||
.map(function (dirPath) {
|
||||
return basename(dirPath);
|
||||
})
|
||||
.sort();
|
||||
log.info("扫描到弹窗目录数量: " + popupDirs.length);
|
||||
|
||||
var popupSetting = settings.find(function (s) {
|
||||
return s.name === "PopupNames";
|
||||
});
|
||||
if (popupSetting) {
|
||||
log.info("找到PopupNames配置项");
|
||||
var existingOptions = popupSetting.options || [];
|
||||
log.info("现有options数量: " + existingOptions.length);
|
||||
|
||||
var existingSet = {};
|
||||
for (var k = 0; k < existingOptions.length; k++) {
|
||||
existingSet[existingOptions[k]] = true;
|
||||
}
|
||||
|
||||
var popupSet = {};
|
||||
for (var p = 0; p < popupDirs.length; p++) {
|
||||
popupSet[popupDirs[p]] = true;
|
||||
}
|
||||
|
||||
var newOptions = [];
|
||||
var removedOptions = [];
|
||||
for (var m = 0; m < popupDirs.length; m++) {
|
||||
if (!existingSet[popupDirs[m]]) {
|
||||
newOptions.push(popupDirs[m]);
|
||||
}
|
||||
}
|
||||
for (var n = 0; n < existingOptions.length; n++) {
|
||||
if (!popupSet[existingOptions[n]]) {
|
||||
removedOptions.push(existingOptions[n]);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("新增options数量: " + newOptions.length);
|
||||
log.info("删除options数量: " + removedOptions.length);
|
||||
|
||||
if (newOptions.length > 0 || removedOptions.length > 0) {
|
||||
popupSetting.options = popupDirs;
|
||||
hasChanges = true;
|
||||
if (newOptions.length > 0) {
|
||||
log.info("PopupNames新增选项: " + newOptions.join(', '));
|
||||
}
|
||||
if (removedOptions.length > 0) {
|
||||
log.info("PopupNames删除选项: " + removedOptions.join(', '));
|
||||
}
|
||||
} else {
|
||||
log.info("PopupNames无新增选项");
|
||||
}
|
||||
} else {
|
||||
log.info("未找到PopupNames配置项");
|
||||
}
|
||||
|
||||
var cdCategories = readAllFilePaths("materialsCD", 0, 1, ['.txt'])
|
||||
.map(function (filePath) {
|
||||
return basename(filePath).replace('.txt', '');
|
||||
})
|
||||
.sort();
|
||||
|
||||
var cdSetting = settings.find(function (s) {
|
||||
return s.name === "CDCategories";
|
||||
});
|
||||
if (cdSetting) {
|
||||
var existingOptions = cdSetting.options || [];
|
||||
var existingSet = {};
|
||||
for (var k = 0; k < existingOptions.length; k++) {
|
||||
existingSet[existingOptions[k]] = true;
|
||||
}
|
||||
var cdSet = {};
|
||||
for (var p = 0; p < cdCategories.length; p++) {
|
||||
cdSet[cdCategories[p]] = true;
|
||||
}
|
||||
var newOptions = [];
|
||||
var removedOptions = [];
|
||||
for (var m = 0; m < cdCategories.length; m++) {
|
||||
if (!existingSet[cdCategories[m]]) {
|
||||
newOptions.push(cdCategories[m]);
|
||||
}
|
||||
}
|
||||
for (var n = 0; n < existingOptions.length; n++) {
|
||||
if (!cdSet[existingOptions[n]]) {
|
||||
removedOptions.push(existingOptions[n]);
|
||||
}
|
||||
}
|
||||
if (newOptions.length > 0 || removedOptions.length > 0) {
|
||||
cdSetting.options = cdCategories;
|
||||
hasChanges = true;
|
||||
if (newOptions.length > 0) {
|
||||
log.info("CDCategories新增选项: " + newOptions.join(', '));
|
||||
}
|
||||
if (removedOptions.length > 0) {
|
||||
log.info("CDCategories删除选项: " + removedOptions.join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pickCategories = readAllFilePaths("targetText", 0, 1, ['.txt'])
|
||||
.map(function (filePath) {
|
||||
return basename(filePath).replace('.txt', '');
|
||||
})
|
||||
.sort();
|
||||
|
||||
var pickSetting = settings.find(function (s) {
|
||||
return s.name === "PickCategories";
|
||||
});
|
||||
if (pickSetting) {
|
||||
var existingOptions = pickSetting.options || [];
|
||||
var existingSet = {};
|
||||
for (var k = 0; k < existingOptions.length; k++) {
|
||||
existingSet[existingOptions[k]] = true;
|
||||
}
|
||||
var pickSet = {};
|
||||
for (var p = 0; p < pickCategories.length; p++) {
|
||||
pickSet[pickCategories[p]] = true;
|
||||
}
|
||||
var newOptions = [];
|
||||
var removedOptions = [];
|
||||
for (var m = 0; m < pickCategories.length; m++) {
|
||||
if (!existingSet[pickCategories[m]]) {
|
||||
newOptions.push(pickCategories[m]);
|
||||
}
|
||||
}
|
||||
for (var n = 0; n < existingOptions.length; n++) {
|
||||
if (!pickSet[existingOptions[n]]) {
|
||||
removedOptions.push(existingOptions[n]);
|
||||
}
|
||||
}
|
||||
if (newOptions.length > 0 || removedOptions.length > 0) {
|
||||
pickSetting.options = pickCategories;
|
||||
hasChanges = true;
|
||||
if (newOptions.length > 0) {
|
||||
log.info("PickCategories新增选项: " + newOptions.join(', '));
|
||||
}
|
||||
if (removedOptions.length > 0) {
|
||||
log.info("PickCategories删除选项: " + removedOptions.join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
var updatedContent = JSON.stringify(settings, null, 2);
|
||||
file.writeTextSync(SETTINGS_FILE, updatedContent, false);
|
||||
log.info("settings.json已自动更新");
|
||||
} else {
|
||||
log.info("settings.json无需更新");
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("自动更新settings.json失败: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateSettingsOptions();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "背包材料统计及采集管理系统",
|
||||
"version": "2.60",
|
||||
"name": "背包统计采集系统",
|
||||
"version": "2.61",
|
||||
"bgi_version": "0.55",
|
||||
"description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md",
|
||||
"description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS,给路径名批量添加检测码,方便识别。",
|
||||
"saved_files": [
|
||||
"pathing/",
|
||||
"history_record/",
|
||||
|
||||
@@ -1,105 +1,135 @@
|
||||
[
|
||||
{
|
||||
"name": "TargetCount",
|
||||
"type": "input-text",
|
||||
"label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
},
|
||||
{
|
||||
"name": "TargetresourceName",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n优先级材料,跳过目标数直接运行\n如填入 甜甜花,薄荷,苹果"
|
||||
},
|
||||
{
|
||||
"name": "TimeCost",
|
||||
"type": "input-text",
|
||||
"label": "====================\n时间成本:秒\n一单位材料的平均耗时,默认30"
|
||||
},
|
||||
{
|
||||
"name": "notify",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n是否发送通知。默认:否\n需在BGI开启JS通知,并设置通知地址"
|
||||
},
|
||||
{
|
||||
"name": "noRecord",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n取消扫描。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)"
|
||||
},
|
||||
{
|
||||
"name": "Pathing",
|
||||
"type": "select",
|
||||
"label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:仅📁pathing材料",
|
||||
"options": [
|
||||
"1.兼并:📁pathing材料+【材料分类】",
|
||||
"2.仅📁pathing材料",
|
||||
"3.仅【材料分类】勾选"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Categories",
|
||||
"type": "multi-checkbox",
|
||||
"label": "\n----------------------------------\n【材料分类】",
|
||||
"options": [
|
||||
"矿石、原胚",
|
||||
"经验书、怪物掉落",
|
||||
"采集食物",
|
||||
"一般素材",
|
||||
"烹饪用食材",
|
||||
"周本素材",
|
||||
"木材",
|
||||
"世界BOSS",
|
||||
"鱼饵、鱼类",
|
||||
"宝石",
|
||||
"天赋素材",
|
||||
"武器突破"
|
||||
],
|
||||
"default": [
|
||||
"一般素材",
|
||||
"烹饪用食材"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PopupNames",
|
||||
"type": "input-text",
|
||||
"label": "数字太小可能无法识别,用?代替\n====================\n弹窗名(默认:全部)"
|
||||
},
|
||||
{
|
||||
"name": "PopupClickDelay",
|
||||
"type": "input-text",
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)"
|
||||
},
|
||||
{
|
||||
"name": "CDCategories",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n\n采用的CD分类(默认:全部) 举例:采集,怪物,木材"
|
||||
},
|
||||
{
|
||||
"name": "CurrentTime",
|
||||
"type": "input-text",
|
||||
"label": "根据CD分类来加载路径文件,具体看materialsCD目录\n====================\n\n终止时刻(默认:不执行) 例:4:00"
|
||||
},
|
||||
{
|
||||
"name": "PickCategories",
|
||||
"type": "input-text",
|
||||
"label": "====================\n\n采用的识别名单(默认:全部) 举例:交互,采集,宝箱"
|
||||
},
|
||||
{
|
||||
"name": "ExceedCount",
|
||||
"type": "input-text",
|
||||
"label": "举例:交互,采集,宝箱\n----------------------------------\n超量阈值(默认:9000)超量的路径材料将不拾取"
|
||||
},
|
||||
{
|
||||
"name": "HoldX",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n(0,0)———> X 增加\n |\n |\n V Y 增加\n\n翻页拖动点X坐标:0~1920(默认1050)"
|
||||
},
|
||||
{
|
||||
"name": "HoldY",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n拖动距离:(默认711像素点)"
|
||||
}
|
||||
[
|
||||
{
|
||||
"name": "TargetCount",
|
||||
"type": "input-text",
|
||||
"label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/📁背包材料统计/\n 📁pathing/\n 📁 食材与炼金/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
},
|
||||
{
|
||||
"name": "TargetresourceName",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n优先级材料,跳过目标数直接运行\n如填入 甜甜花,薄荷,苹果"
|
||||
},
|
||||
{
|
||||
"name": "TimeCost",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n时间成本:秒\n一单位材料的平均耗时,默认30"
|
||||
},
|
||||
{
|
||||
"name": "PickCategories",
|
||||
"type": "multi-checkbox",
|
||||
"label": "====================\n\n采用的识别名单(默认:全部)\n不勾选,则默认",
|
||||
"options": [
|
||||
"交互",
|
||||
"宝箱",
|
||||
"掉落",
|
||||
"采集"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ExceedCount",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n超量阈值(默认:9000),超量的路径材料将也不拾取\n普通材料超量,或者怪物掉落的三个材料都超量,则跳过其路径"
|
||||
},
|
||||
{
|
||||
"name": "notify",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n是否发送通知。默认:否\n需在BGI开启JS通知,并设置通知地址"
|
||||
},
|
||||
{
|
||||
"name": "noRecord",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n取消扫描。默认:否\n勾选将不进行单路径的扫描,但保留运行时间记录\n(推荐路径记录炼成后启用)"
|
||||
},
|
||||
{
|
||||
"name": "Pathing",
|
||||
"type": "multi-checkbox",
|
||||
"label": "====================\n统计选择:📁pathing下的材料,或【扫描额外的分类】的材料。默认:仅📁pathing材料",
|
||||
"options": [
|
||||
"📁pathing材料",
|
||||
"【扫描额外的分类】"
|
||||
],
|
||||
"default": [
|
||||
"📁pathing材料"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Categories",
|
||||
"type": "multi-checkbox",
|
||||
"label": "\n----------------------------------\n【扫描的分类】",
|
||||
"options": [
|
||||
"矿石、原胚",
|
||||
"经验书、怪物掉落",
|
||||
"采集食物",
|
||||
"一般素材",
|
||||
"烹饪用食材",
|
||||
"周本素材",
|
||||
"木材",
|
||||
"世界BOSS",
|
||||
"鱼饵、鱼类",
|
||||
"宝石",
|
||||
"天赋素材",
|
||||
"武器突破"
|
||||
],
|
||||
"default": [
|
||||
"一般素材",
|
||||
"烹饪用食材"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PopupNames",
|
||||
"type": "multi-checkbox",
|
||||
"label": "数字太小可能无法识别,用?代替\n\n====================\n弹窗名(默认:全部),不勾选,则默认。",
|
||||
"options": [
|
||||
"信件",
|
||||
"冻结",
|
||||
"复苏",
|
||||
"对话",
|
||||
"月卡",
|
||||
"登录",
|
||||
"调查",
|
||||
"过期物品"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PopupClickDelay",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n弹窗循环间隔(默认:15 秒)"
|
||||
},
|
||||
{
|
||||
"name": "CDCategories",
|
||||
"type": "multi-checkbox",
|
||||
"label": "====================\n\n根据CD分类,选择加载路径文件(默认:全部)\n不勾选,则默认。",
|
||||
"options": [
|
||||
"怪物",
|
||||
"掉落",
|
||||
"木材",
|
||||
"狗粮",
|
||||
"采集"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CurrentTime",
|
||||
"type": "input-text",
|
||||
"label": "\n\n----------------------------------\n\n终止时刻(默认:不执行) 例:4:00"
|
||||
},
|
||||
{
|
||||
"name": "HoldX",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n(0,0)———> X 增加\n |\n |\n V Y 增加\n\n翻页拖动点X坐标:0~1920(默认1050)"
|
||||
},
|
||||
{
|
||||
"name": "HoldY",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n拖动距离:(默认711像素点)推荐一次划页稍小于4行材料的距离"
|
||||
},
|
||||
{
|
||||
"name": "debugLog",
|
||||
"type": "checkbox",
|
||||
"label": "------------------------\n调试日志。默认:否\n输出详细的调试信息"
|
||||
}
|
||||
]
|
||||
@@ -6,5 +6,3 @@
|
||||
|
||||
矿石:萃凝晶,紫晶块,星银矿石,魔晶块,水晶块,白铁块,铁块,虹滴晶,
|
||||
|
||||
狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user