修复凌晨CD计算bug,修复超量名单潜在记录污染bug (#2935)

This commit is contained in:
吉吉喵
2026-02-27 13:25:13 +08:00
committed by GitHub
parent b5a7d30c29
commit 6f63acbc53
3 changed files with 263 additions and 226 deletions

View File

@@ -1,4 +1,4 @@
# 背包材料统计 v2.61
# 背包材料统计 v2.62
作者:吉吉喵
<!-- 新增:全局图片样式,控制连续图片同行显示 -->
@@ -233,4 +233,5 @@ A记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
| v2.58 | 优化背包扫图逻辑 |
| v2.59 | 修复自动拾取匹配bug改为双向匹配 |
| v2.60 | 手动终止路径会被记录成noRecord模式只参与CD计算增加当前路线预估时长日志材料分类升级多选框UI刚需bgi v0.55版本优化文件是否存在逻辑降级ReadTextSync报错检测码识别路径 |
| v2.61 | 背包材料识别机制加速修复手动终止路径noRecord模式的额外条件判断不生效全局图片缓存识别名单、CD文件和弹窗升级多选框UI注意UI修改了配置组里需要删除并重新添加该js |
| v2.61 | 背包材料识别机制加速修复手动终止路径noRecord模式的额外条件判断不生效全局图片缓存识别名单、CD文件和弹窗升级多选框UI注意UI修改了配置组里需要删除并重新添加该js |
| v2.62 | 修复凌晨CD计算bug修复超量名单潜在记录污染bug | |

View File

@@ -10,7 +10,7 @@ const CONSTANTS = {
NO_RECORD_DIR: "pathing_record/noRecord",
IMAGES_DIR: "assets/images",
MONSTER_MATERIALS_PATH: "assets/Monster-Materials.txt",
// 解析与处理配置
MAX_PATH_DEPTH: 3, // 路径解析最大深度
NOTIFICATION_CHUNK_SIZE: 500, // 通知拆分长度
@@ -18,7 +18,7 @@ const CONSTANTS = {
FOOD_ZERO_EXP_SUFFIX: "_狗粮-0.txt", // 新增狗粮0 EXP记录后缀
SUMMARY_FILE_NAME: "材料收集汇总.txt",
ZERO_COUNT_SUFFIX: "-0.txt",
// 日志模块标识
LOG_MODULES: {
INIT: "[初始化]",
@@ -91,22 +91,22 @@ function generatePathContentCode(pathingFilePath) {
if (extractedCode) {
return extractedCode;
}
// 如果文件名中没有检测码,生成新的
const content = safeReadTextSync(pathingFilePath);
if (!content) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件为空: ${pathingFilePath}`);
return "00000000";
}
const pathData = JSON.parse(content);
const positions = pathData.positions || pathData.actions || [];
if (!Array.isArray(positions) || positions.length === 0) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}路径文件无有效位置数据: ${pathingFilePath}`);
return "00000000";
}
return generateContentCode(positions);
} catch (error) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}生成路径检测码失败: ${error.message}`);
@@ -125,14 +125,14 @@ var state = { completed: false, cancelRequested: false, ocrPaused: false };
const globalImageCache = new Map();
function getCachedImageMat(filePath) {
if (globalImageCache.has(filePath)) {
return globalImageCache.get(filePath);
}
const mat = file.readImageMatSync(filePath);
if (!mat.empty()) {
globalImageCache.set(filePath, mat);
}
return mat;
if (globalImageCache.has(filePath)) {
return globalImageCache.get(filePath);
}
const mat = file.readImageMatSync(filePath);
if (!mat.empty()) {
globalImageCache.set(filePath, mat);
}
return mat;
}
// ==============================================
@@ -149,34 +149,34 @@ const noRecord = settings.noRecord || false;
const debugLog = settings.debugLog || false;
const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量
const exceedCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.ExceedCount) || 9000))); // 设定的超量目标数量
const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null;
const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null;
// 解析需要处理的CD分类
let allowedCDCategories = [];
try {
allowedCDCategories = Array.from(settings.CDCategories || []);
allowedCDCategories = Array.from(settings.CDCategories || []);
} catch (e) {
log.error(`${CONSTANTS.LOG_MODULES.INIT}获取CDCategories设置失败: ${e.message}`);
log.error(`${CONSTANTS.LOG_MODULES.INIT}获取CDCategories设置失败: ${e.message}`);
}
let availableCDCategories = [];
try {
const cdFilePaths = readAllFilePaths(CONSTANTS.MATERIAL_CD_DIR, 0, 1, ['.txt']);
availableCDCategories = cdFilePaths.map(filePath => basename(filePath).replace('.txt', ''));
log.info(`${CONSTANTS.LOG_MODULES.INIT}可用CD分类${availableCDCategories.join(', ')}`);
const cdFilePaths = readAllFilePaths(CONSTANTS.MATERIAL_CD_DIR, 0, 1, ['.txt']);
availableCDCategories = cdFilePaths.map(filePath => basename(filePath).replace('.txt', ''));
log.info(`${CONSTANTS.LOG_MODULES.INIT}可用CD分类${availableCDCategories.join(', ')}`);
} catch (e) {
log.error(`${CONSTANTS.LOG_MODULES.INIT}扫描CD目录失败: ${e.message}`);
log.error(`${CONSTANTS.LOG_MODULES.INIT}扫描CD目录失败: ${e.message}`);
}
if (allowedCDCategories.length > 0) {
const invalidCategories = allowedCDCategories.filter(cat => !availableCDCategories.includes(cat));
if (invalidCategories.length > 0) {
log.warn(`${CONSTANTS.LOG_MODULES.INIT}以下CD分类不存在将被忽略${invalidCategories.join('、')}`);
allowedCDCategories = allowedCDCategories.filter(cat => availableCDCategories.includes(cat));
}
log.info(`${CONSTANTS.LOG_MODULES.INIT}已配置只处理以下CD分类${allowedCDCategories.join('、')}`);
const invalidCategories = allowedCDCategories.filter(cat => !availableCDCategories.includes(cat));
if (invalidCategories.length > 0) {
log.warn(`${CONSTANTS.LOG_MODULES.INIT}以下CD分类不存在将被忽略${invalidCategories.join('、')}`);
allowedCDCategories = allowedCDCategories.filter(cat => availableCDCategories.includes(cat));
}
log.info(`${CONSTANTS.LOG_MODULES.INIT}已配置只处理以下CD分类${allowedCDCategories.join('、')}`);
} else {
log.info(`${CONSTANTS.LOG_MODULES.INIT}未配置CD分类过滤将处理所有分类`);
log.info(`${CONSTANTS.LOG_MODULES.INIT}未配置CD分类过滤将处理所有分类`);
}
// ==============================================
@@ -212,14 +212,14 @@ function parseMonsterMaterials() {
try {
const content = safeReadTextSync(CONSTANTS.MONSTER_MATERIALS_PATH);
const lines = content.split('\n').map(line => line.trim()).filter(line => line);
lines.forEach(line => {
if (!line.includes('')) return;
const [monsterName, materialsStr] = line.split('');
const materials = materialsStr.split(/[,,、 \s]+/)
.map(mat => mat.trim())
.filter(mat => mat);
if (monsterName && materials.length > 0) {
monsterToMaterials[monsterName] = materials;
materials.forEach(mat => {
@@ -262,17 +262,17 @@ if (pathingMode.onlyPathing) log.warn(`${CONSTANTS.LOG_MODULES.PATH}已开启【
/**
* 初始化并筛选选中的材料分类适配multi-checkbox的Categories配置
* @returns {string[]} 选中的材料分类列表
*/
function getSelectedMaterialCategories() {
*/
function getSelectedMaterialCategories() {
// 使用Array.from()确保将settings.Categories转换为真正的数组适配multi-checkbox返回的类数组对象
let selectedCategories = [];
try {
selectedCategories = Array.from(settings.Categories || []);
} catch (e) {
log.error(`${CONSTANTS.LOG_MODULES.MATERIAL}获取分类设置失败: ${e.message}`);
}
// 兼容旧的checkbox字段名
if (!selectedCategories || selectedCategories.length === 0) {
const checkboxToCategory = {
@@ -290,25 +290,25 @@ function getSelectedMaterialCategories() {
"WeaponAscension": "武器突破",
"XP": "祝圣精华"
};
Object.keys(checkboxToCategory).forEach(checkboxName => {
if (settings[checkboxName] === true) {
selectedCategories.push(checkboxToCategory[checkboxName]);
}
});
}
// 默认分类
if (!selectedCategories || selectedCategories.length === 0) {
selectedCategories = ["一般素材", "烹饪用食材"];
}
// 过滤无效值并映射到实际分类
return selectedCategories
.filter(cat => typeof cat === 'string' && cat !== "")
.map(name => material_mapping[name] || "锻造素材")
.filter(name => name !== null);
}
return selectedCategories
.filter(cat => typeof cat === 'string' && cat !== "")
.map(name => material_mapping[name] || "锻造素材")
.filter(name => name !== null);
}
const selected_materials_array = getSelectedMaterialCategories();
@@ -630,10 +630,10 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath) {
const checkDirs = [recordDir, noRecordDir];
let latestEndTime = null;
// 生成内容检测码
const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : null;
// 清理路径名中的检测码
const cleanPathName = pathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json');
@@ -645,13 +645,13 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi
// 按空行分割成记录块
const recordBlocks = content.split('\n\n').filter(block => block.includes('路径名: '));
recordBlocks.forEach(block => {
const blockLines = block.split('\n');
let blockPathName = '';
let blockContentCode = '00000000';
let blockEndTime = null;
blockLines.forEach(line => {
if (line.startsWith('路径名: ')) {
blockPathName = line.split('路径名: ')[1];
@@ -661,15 +661,15 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi
blockEndTime = line.split('结束时间: ')[1];
}
});
// 清理记录中的路径名检测码
const cleanBlockPathName = blockPathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json');
// 匹配条件:路径名相同 或者 内容检测码相同(新逻辑)
const isPathMatch = cleanBlockPathName === cleanPathName;
const isContentCodeMatch = contentCode && blockContentCode === contentCode;
const isMatch = isPathMatch || isContentCodeMatch;
if (isMatch && blockEndTime) {
const endTime = new Date(blockEndTime);
if (!latestEndTime || endTime > new Date(latestEndTime)) {
@@ -698,10 +698,10 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathi
function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir, isFood = false, cache = {}, pathingFilePath) {
// 生成内容检测码
const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : null;
// 清理路径名中的检测码
const cleanPathName = pathName.replace(/_[0-9a-fA-F]{8}\.json$/, '.json');
// 1. 生成唯一缓存键(确保不同路径/不同文件的记录不混淆)
const isFoodSuffix = isFood ? CONSTANTS.FOOD_EXP_RECORD_SUFFIX : ".txt";
const recordFile = `${recordDir}/${resourceKey}${isFoodSuffix}`;
@@ -738,7 +738,7 @@ function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir,
const lines = content.split('\n');
// 先按空行分割成独立的记录块,避免跨记录解析
const recordBlocks = content.split('\n\n').filter(block => block.includes('路径名: '));
recordBlocks.forEach(block => {
const blockLines = block.split('\n').map(line => line.trim()).filter(line => line);
let runTime = 0;
@@ -781,7 +781,7 @@ function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir,
// 匹配条件:路径名相同 或者 内容检测码相同(新逻辑)
const isContentCodeMatch = contentCode && recordContentCode === contentCode;
const shouldInclude = (isTargetPath || isContentCodeMatch) && runTime > 0;
if (shouldInclude) {
records.push({ runTime, quantityChange, contentCode: recordContentCode });
}
@@ -815,11 +815,11 @@ function estimatePathTotalTime(entry, recordDir, noRecordDir, cache = {}) {
// 调用公共函数获取记录(复用缓存)
const historicalRecords = getHistoricalPathRecords(
resourceKey,
pathName,
recordDir,
noRecordDir,
isFood,
resourceKey,
pathName,
recordDir,
noRecordDir,
isFood,
cache,
pathingFilePath
);
@@ -854,11 +854,11 @@ function calculatePerTime(resourceName, pathName, recordDir, noRecordDir, isFood
const isMonster = monsterToMaterials.hasOwnProperty(resourceName);
// 调用公共函数获取记录(复用缓存)
const historicalRecords = getHistoricalPathRecords(
resourceName,
pathName,
recordDir,
noRecordDir,
isFood,
resourceName,
pathName,
recordDir,
noRecordDir,
isFood,
cache,
pathingFilePath
);
@@ -873,7 +873,7 @@ function calculatePerTime(resourceName, pathName, recordDir, noRecordDir, isFood
if (isMonster) {
// 怪物路径:按中级单位计算
const monsterMaterials = monsterToMaterials[resourceName];
const gradeRatios = [3, 1, 1 / 3]; // 最高级×3中级×1最低级×1/3
const gradeRatios = [3, 1, 1/3]; // 最高级×3中级×1最低级×1/3
historicalRecords.forEach(record => {
const { runTime, quantityChange } = record;
@@ -955,18 +955,28 @@ function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) {
log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRunTime.toLocaleString()}`);
return canRun;
} else if (refreshCD.type === 'specific') {
const specificHour = refreshCD.hour;
const currentDate = new Date();
const lastDate = new Date(lastEndTimeDate);
const todayRefresh = new Date(currentDate);
todayRefresh.setHours(specificHour, 0, 0, 0);
if (currentDate > todayRefresh && currentDate.getDate() !== lastDate.getDate()) {
return true;
}
const nextRefreshTime = new Date(todayRefresh);
if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1);
log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`);
return false;
const specificHour = refreshCD.hour;
// 计算上次运行后最近的刷新时间
const lastRefreshAfterRun = new Date(lastEndTimeDate);
lastRefreshAfterRun.setHours(specificHour, 0, 0, 0);
if (lastRefreshAfterRun <= lastEndTimeDate) {
lastRefreshAfterRun.setDate(lastRefreshAfterRun.getDate() + 1);
}
// 如果当前时间已经过了最近的刷新时间,允许运行
if (currentDate >= lastRefreshAfterRun) {
log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},最近刷新时间:${lastRefreshAfterRun.toLocaleString()},允许运行`);
return true;
}
// 计算下次刷新时间
const todayRefresh = new Date(currentDate);
todayRefresh.setHours(specificHour, 0, 0, 0);
const nextRefreshTime = new Date(todayRefresh);
if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1);
log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`);
return false;
} else if (refreshCD.type === 'instant') {
return true;
}
@@ -998,7 +1008,7 @@ const imageMapCache = new Map(); // 保持固定,不动态刷新
const createImageCategoryMap = (imagesDir) => {
const map = {};
const imageFiles = readAllFilePaths(imagesDir, 0, 1, ['.png']);
for (const imagePath of imageFiles) {
const pathParts = imagePath.split(/[\\/]/);
if (pathParts.length < 3) continue;
@@ -1007,12 +1017,12 @@ const createImageCategoryMap = (imagesDir) => {
.replace(/\.png$/i, '')
.trim()
.toLowerCase();
if (!(imageName in map)) {
map[imageName] = pathParts[2];
}
}
return map;
};
@@ -1026,7 +1036,7 @@ const loggedResources = new Set();
*/
function matchImageAndGetCategory(resourceName, imagesDir) {
const processedName = (MATERIAL_ALIAS[resourceName] || resourceName).toLowerCase();
if (!imageMapCache.has(imagesDir)) {
log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}初始化图像分类缓存:${imagesDir}`);
imageMapCache.set(imagesDir, createImageCategoryMap(imagesDir));
@@ -1042,7 +1052,7 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
if (!loggedResources.has(processedName)) {
loggedResources.add(processedName);
}
return result;
}
@@ -1179,22 +1189,22 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir,
const currentTime = getCurrentTimeInHours();
const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath);
const perTime = noRecord ? null : calculatePerTime(
resourceName,
pathName,
recordDir,
noRecordDir,
resourceName,
pathName,
recordDir,
noRecordDir,
true,
pathRecordCache,
pathingFilePath
);
log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 单位EXP耗时${perTime ?? '忽略'}秒/EXP`);
const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir);
log.info(`${CONSTANTS.LOG_MODULES.PATH}狗粮路径${pathName} 预计耗时:${estimatedTime}`);
const canRun = canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName)
&& isPathValid
const canRun = canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName)
&& isPathValid
&& (noRecord || perTime === null || perTime <= timeCost);
if (!canRun) {
@@ -1252,7 +1262,7 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir,
finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
runTime = (new Date(endTime) - new Date(startTime)) / 1000;
const canRecord = runTime > 5 && finalCumulativeDistance > 5;
if (canRecord) {
const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000";
@@ -1276,9 +1286,9 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir,
async function processMonsterPathEntry(entry, context) {
const { path: pathingFilePath, monsterName } = entry;
const pathName = basename(pathingFilePath);
const {
const {
CDCategories, timeCost, recordDir, noRecordDir, imagesDir,
materialCategoryMap, flattenedLowCountMaterials,
materialCategoryMap, flattenedLowCountMaterials,
currentMaterialName: prevMaterialName,
materialAccumulatedDifferences, globalAccumulatedDifferences,
pathRecordCache
@@ -1321,17 +1331,17 @@ async function processMonsterPathEntry(entry, context) {
const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir, pathingFilePath);
const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(
monsterName,
pathName,
recordDir,
noRecordDir,
monsterName,
pathName,
recordDir,
noRecordDir,
false,
pathRecordCache,
pathingFilePath
);
log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`);
const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, monsterName }, recordDir, noRecordDir);
log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 预计耗时:${estimatedTime}`);
@@ -1423,7 +1433,17 @@ async function processMonsterPathEntry(entry, context) {
});
log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}怪物路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`);
recordRunTime(monsterName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath);
// 检查怪物对应的材料是否有超量如果有记录到noRecord目录
let isExcess = false;
const monsterMaterials = monsterToMaterials[monsterName] || [];
for (const material of monsterMaterials) {
if (excessMaterialNames.includes(material)) {
isExcess = true;
break;
}
}
const targetRecordDir = isExcess ? noRecordDir : recordDir;
recordRunTime(monsterName, pathName, startTime, endTime, runTime, targetRecordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath);
}
await sleep(1);
@@ -1440,7 +1460,7 @@ async function processMonsterPathEntry(entry, context) {
finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
runTime = (new Date(endTime) - new Date(startTime)) / 1000;
const canRecord = runTime > 5 && finalCumulativeDistance > 5;
if (canRecord) {
const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000";
@@ -1464,9 +1484,9 @@ async function processMonsterPathEntry(entry, context) {
async function processNormalPathEntry(entry, context) {
const { path: pathingFilePath, resourceName } = entry;
const pathName = basename(pathingFilePath);
const {
const {
CDCategories, timeCost, recordDir, noRecordDir,
materialCategoryMap, flattenedLowCountMaterials,
materialCategoryMap, flattenedLowCountMaterials,
currentMaterialName: prevMaterialName,
materialAccumulatedDifferences, globalAccumulatedDifferences,
pathRecordCache
@@ -1501,17 +1521,17 @@ async function processNormalPathEntry(entry, context) {
const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir, pathingFilePath);
const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(
resourceName,
pathName,
recordDir,
noRecordDir,
resourceName,
pathName,
recordDir,
noRecordDir,
false,
pathRecordCache,
pathingFilePath
);
log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`);
const estimatedTime = estimatePathTotalTime({ path: pathingFilePath, resourceName }, recordDir, noRecordDir);
log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 预计耗时:${estimatedTime}`);
@@ -1594,7 +1614,23 @@ async function processNormalPathEntry(entry, context) {
});
log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}材料路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`);
recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath);
// 检查材料是否在超量名单中如果是记录到noRecord目录
let isExcess = false;
// 检查当前材料是否超量
if (excessMaterialNames.includes(resourceName)) {
isExcess = true;
} else if (monsterToMaterials.hasOwnProperty(resourceName)) {
// 对于怪物路径,检查其对应的材料是否有超量
const monsterMaterials = monsterToMaterials[resourceName];
for (const material of monsterMaterials) {
if (excessMaterialNames.includes(material)) {
isExcess = true;
break;
}
}
}
const targetRecordDir = isExcess ? noRecordDir : recordDir;
recordRunTime(resourceName, pathName, startTime, endTime, runTime, targetRecordDir, materialCountDifferences, finalCumulativeDistance, pathingFilePath);
}
await sleep(1);
@@ -1611,7 +1647,7 @@ async function processNormalPathEntry(entry, context) {
finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
const endTime = new Date().toLocaleString();
runTime = (new Date(endTime) - new Date(startTime)) / 1000;
const canRecord = runTime > 5 && finalCumulativeDistance > 5;
if (canRecord) {
const contentCode = pathingFilePath ? generatePathContentCode(pathingFilePath) : "00000000";
@@ -1647,7 +1683,7 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
const globalAccumulatedDifferences = {};
const materialAccumulatedDifferences = {};
// 单路径处理周期内的记录缓存
const pathRecordCache = {};
const pathRecordCache = {};
let context = {
CDCategories, timeCost, recordDir, noRecordDir, imagesDir,
materialCategoryMap, flattenedLowCountMaterials,
@@ -1676,9 +1712,9 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
}
const pathTotalTimeSec = estimatePathTotalTime(
entry,
recordDir,
noRecordDir,
entry,
recordDir,
noRecordDir,
pathRecordCache
);
const pathTotalTimeMin = pathTotalTimeSec / 60;
@@ -1706,15 +1742,15 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
if (resourceName && isFoodResource(resourceName)) {
// 狗粮路径:传递完整校验参数
const result = await processFoodPathEntry(
entry,
entry,
{
foodExpAccumulator,
currentMaterialName: context.currentMaterialName
},
recordDir,
noRecordDir,
CDCategories,
timeCost,
},
recordDir,
noRecordDir,
CDCategories,
timeCost,
context.pathRecordCache
);
foodExpAccumulator = result.foodExpAccumulator;
@@ -1730,7 +1766,7 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
}
} catch (singleError) {
log.error(`${CONSTANTS.LOG_MODULES.PATH}处理路径出错,已跳过:${singleError.message}`);
await sleep(1);
if (state.cancelRequested) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到终止指令,停止处理`);
@@ -1750,8 +1786,8 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
}
}
return {
currentMaterialName: context.currentMaterialName,
return {
currentMaterialName: context.currentMaterialName,
flattenedLowCountMaterials: context.flattenedLowCountMaterials,
globalAccumulatedDifferences: context.globalAccumulatedDifferences,
foodExpAccumulator
@@ -1871,13 +1907,13 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
monsterPaths.forEach((entry, index) => {
const materials = monsterToMaterials[entry.monsterName] || [];
if (materials.length === 0) {
log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物路径${index + 1}] 怪物【${entry.monsterName}】无对应材料映射`);
log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物路径${index+1}] 怪物【${entry.monsterName}】无对应材料映射`);
return;
}
materials.forEach(mat => {
// 添加到pathing怪物材料集合用于OCR过滤
ocrContext.pathingMonsterMaterials.add(mat);
const category = matchImageAndGetCategory(mat, imagesDir);
if (!category) return;
if (!materialCategoryMap[category]) materialCategoryMap[category] = [];
@@ -1924,41 +1960,41 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
// 路径优先级规则数组
const PATH_PRIORITIES = [
// 1. 目标狗粮
{
source: processedFoodPaths,
filter: e => targetResourceNames.includes(e.resourceName)
{
source: processedFoodPaths,
filter: e => targetResourceNames.includes(e.resourceName)
},
// 2. 目标怪物(掉落材料含目标)
{
source: processedMonsterPaths,
{
source: processedMonsterPaths,
filter: e => {
const materials = monsterToMaterials[e.monsterName] || [];
return materials.some(mat => targetResourceNames.includes(mat));
}
}
},
// 3. 目标普通材料
{
source: processedNormalPaths,
filter: e => targetResourceNames.includes(e.resourceName)
{
source: processedNormalPaths,
filter: e => targetResourceNames.includes(e.resourceName)
},
// 4. 剩余狗粮
{
source: processedFoodPaths,
filter: e => !targetResourceNames.includes(e.resourceName)
{
source: processedFoodPaths,
filter: e => !targetResourceNames.includes(e.resourceName)
},
// 5. 剩余怪物(掉落材料未超量且低数量)
{
source: processedMonsterPaths,
{
source: processedMonsterPaths,
filter: e => {
const materials = monsterToMaterials[e.monsterName] || [];
return !materials.some(mat => targetResourceNames.includes(mat)) &&
materials.some(mat => !excessMaterialNames.includes(mat));
}
return !materials.some(mat => targetResourceNames.includes(mat)) &&
materials.some(mat => !excessMaterialNames.includes(mat));
}
},
// 6. 剩余普通材料
{
source: processedNormalPaths,
filter: e => !targetResourceNames.includes(e.resourceName)
{
source: processedNormalPaths,
filter: e => !targetResourceNames.includes(e.resourceName)
}
];
@@ -1967,7 +2003,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
PATH_PRIORITIES.forEach(({ source, filter }, index) => {
const filtered = source.filter(filter);
allPaths.push(...filtered);
log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index + 1}] 路径 ${filtered.length}`);
log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index+1}] 路径 ${filtered.length}`);
});
// log.info(`${CONSTANTS.LOG_MODULES.PATH}[最终路径] 共${allPaths.length}条:${allPaths.map(p => basename(p.path))}`);
@@ -1994,13 +2030,13 @@ function sendNotificationInChunks(msg, sendFn) {
const totalChunks = Math.ceil(msg.length / chunkSize);
log.info(`${CONSTANTS.LOG_MODULES.MAIN}通知消息过长(${msg.length}字符),拆分为${totalChunks}段发送`);
let start = 0;
for (let i = 0; i < totalChunks; i++) {
const end = Math.min(start + chunkSize, msg.length);
const chunkMsg = `【通知${i + 1}/${totalChunks}\n${msg.substring(start, end)}`;
const chunkMsg = `【通知${i+1}/${totalChunks}\n${msg.substring(start, end)}`;
sendFn(chunkMsg);
log.info(`${CONSTANTS.LOG_MODULES.MAIN}已发送第${i + 1}段通知(${chunkMsg.length}字符)`);
log.info(`${CONSTANTS.LOG_MODULES.MAIN}已发送第${i+1}段通知(${chunkMsg.length}字符)`);
start = end;
}
}
@@ -2062,7 +2098,7 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
// 等待超量名单生成
let waitTimes = 0;
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
await sleep(1000);
waitTimes++;
}
@@ -2070,43 +2106,43 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号已退出`);
return;
}
const getFilteredTargetTexts = () => {
let filtered = allTargetTexts.filter(name => !excessMaterialNames.includes(name));
if (ocrContext.currentPathType === 'monster') {
// 怪物路径执行时的过滤逻辑:
// 1. 对于怪物材料,只保留:
// - 当前怪物的材料
// - pathing文件夹中存在且未超量的其他怪物材料
// 2. 非怪物材料保持不变
filtered = filtered.filter(name => {
// 如果不是怪物材料,保留
if (!materialToMonsters[name]) return true;
// 如果是怪物材料,检查是否在允许的列表中
const currentMonsterMaterials = ocrContext.currentTargetMaterials || [];
const pathingMonsterMaterials = Array.from(ocrContext.pathingMonsterMaterials || new Set());
// 保留当前怪物的材料或pathing中的怪物材料
return currentMonsterMaterials.includes(name) || pathingMonsterMaterials.includes(name);
});
if (debugLog) {
const currentMonsterMaterials = ocrContext.currentTargetMaterials || [];
const pathingMonsterMaterials = Array.from(ocrContext.pathingMonsterMaterials || new Set());
const additionalMonsterMaterials = pathingMonsterMaterials.filter(mat =>
const additionalMonsterMaterials = pathingMonsterMaterials.filter(mat =>
!currentMonsterMaterials.includes(mat) && !excessMaterialNames.includes(mat)
);
log.info(`OCR拾取列表怪物路径`);
log.info(` - 当前怪物材料:${currentMonsterMaterials.join('、') || '无'}`);
log.info(` - pathing其他怪物材料未超量${additionalMonsterMaterials.join('、') || '无'}`);
log.info(` - 非怪物材料:${filtered.filter(name => !materialToMonsters[name]).join('、') || '无'}`);
}
}
return filtered;
};
@@ -2132,78 +2168,78 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
if (debugLog) log.info(`${CONSTANTS.LOG_MODULES.CD}CD文件中材料名已过滤${Array.from(cdMaterialNames).join(', ')}`);
// 生成材料分类映射
let materialCategoryMap = {};
// 处理选中的材料分类
if (selected_materials_array.length > 0 && !pathingMode.onlyPathing) {
// 1. 初始化选中的分类onlyPathing模式除外
selected_materials_array.forEach(selectedCategory => {
materialCategoryMap[selectedCategory] = [];
});
let materialCategoryMap = {};
// 处理选中的材料分类
if (selected_materials_array.length > 0 && !pathingMode.onlyPathing) {
// 1. 初始化选中的分类onlyPathing模式除外
selected_materials_array.forEach(selectedCategory => {
materialCategoryMap[selectedCategory] = [];
});
} else {
if (pathingMode.onlyPathing) {
log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}onlyPathing模式将自动扫描pathing材料的实际分类`);
} else {
if (pathingMode.onlyPathing) {
log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}onlyPathing模式将自动扫描pathing材料的实际分类`);
} else {
log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`);
}
log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`);
}
}
// 2. 处理路径相关材料仅includeBoth和onlyPathing模式
if ((pathingMode.includeBoth || pathingMode.onlyPathing) && (Object.keys(materialCategoryMap).length > 0 || pathingMode.onlyPathing)) {
const pathingFilePaths = readAllFilePaths(CONSTANTS.PATHING_DIR, 0, 3, ['.json']);
const pathEntries = pathingFilePaths.map(path => {
const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames);
return { materialName, monsterName };
});
// 2. 处理路径相关材料仅includeBoth和onlyPathing模式
if ((pathingMode.includeBoth || pathingMode.onlyPathing) && (Object.keys(materialCategoryMap).length > 0 || pathingMode.onlyPathing)) {
const pathingFilePaths = readAllFilePaths(CONSTANTS.PATHING_DIR, 0, 3, ['.json']);
const pathEntries = pathingFilePaths.map(path => {
const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames);
return { materialName, monsterName };
});
// 收集所有材料(含怪物掉落
const allMaterials = new Set();
pathEntries.forEach(({ materialName, monsterName }) => {
if (materialName) allMaterials.add(materialName);
if (monsterName) {
(monsterToMaterials[monsterName] || []).forEach(mat => allMaterials.add(mat));
}
});
// 收集所有材料(含怪物掉落)
const allMaterials = new Set();
pathEntries.forEach(({ materialName, monsterName }) => {
if (materialName) allMaterials.add(materialName);
if (monsterName) {
(monsterToMaterials[monsterName] || []).forEach(mat => allMaterials.add(mat));
// 构建分类映射
Array.from(allMaterials).forEach(resourceName => {
const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR);
if (category) {
if (!materialCategoryMap[category]) {
materialCategoryMap[category] = [];
}
});
// 构建分类映射
Array.from(allMaterials).forEach(resourceName => {
const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR);
if (category) {
if (!materialCategoryMap[category]) {
materialCategoryMap[category] = [];
}
if (!materialCategoryMap[category].includes(resourceName)) {
materialCategoryMap[category].push(resourceName);
}
if (!materialCategoryMap[category].includes(resourceName)) {
materialCategoryMap[category].push(resourceName);
}
}
});
}
// 3. 在onlyPathing或onlyCategory模式下保留所有选中的分类
if (pathingMode.onlyPathing || pathingMode.onlyCategory) {
// 对于onlyCategory模式保留所有选中的分类即使是空数组
// 对于onlyPathing模式删除空数组
if (pathingMode.onlyPathing) {
Object.keys(materialCategoryMap).forEach(category => {
if (materialCategoryMap[category].length === 0) {
delete materialCategoryMap[category];
}
});
}
// 3. 在onlyPathing或onlyCategory模式下保留所有选中的分类
if (pathingMode.onlyPathing || pathingMode.onlyCategory) {
// 对于onlyCategory模式保留所有选中的分类即使是空数组
// 对于onlyPathing模式删除空数组
if (pathingMode.onlyPathing) {
Object.keys(materialCategoryMap).forEach(category => {
if (materialCategoryMap[category].length === 0) {
delete materialCategoryMap[category];
}
});
}
}
// 4. 在onlyCategory模式下确保materialCategoryMap只包含选中的分类
if (pathingMode.onlyCategory) {
const selectedCategoriesSet = new Set(selected_materials_array);
const filteredMap = {};
// 只保留选中的分类,即使是空数组
selected_materials_array.forEach(category => {
filteredMap[category] = materialCategoryMap[category] || [];
});
materialCategoryMap = filteredMap;
}
}
// 4. 在onlyCategory模式下确保materialCategoryMap只包含选中的分类
if (pathingMode.onlyCategory) {
const selectedCategoriesSet = new Set(selected_materials_array);
const filteredMap = {};
// 只保留选中的分类,即使是空数组
selected_materials_array.forEach(category => {
filteredMap[category] = materialCategoryMap[category] || [];
});
materialCategoryMap = filteredMap;
}
// 生成路径数组
const { allPaths, pathingMaterialCounts } = await generateAllPaths(

View File

@@ -1,9 +1,9 @@
{
"manifest_version": 1,
"name": "背包统计采集系统",
"version": "2.61",
"version": "2.62",
"bgi_version": "0.55",
"description": "可统计背包养成道具、部分食物、素材的数量根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势\n+ 1. 自动判断材料CD不需要管材料CD有没有好\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别不会交互路边的npc或是神像可自定义识别名单具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料自动屏蔽更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS给路径名批量添加检测码方便识别。",
"description": "可统计背包养成道具、部分食物、素材的数量根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势\n+ 1. 自动判断材料CD不需要管材料CD有没有好\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别不会交互路边的npc或是神像可自定义识别名单具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料自动屏蔽更多详细内容查看readme.md;可在我的主页下载 路径重命名 工具JS给路径名批量添加检测码方便识别。",
"saved_files": [
"pathing/",
"history_record/",
@@ -13,8 +13,8 @@
],
"authors": [
{
"name": "吉吉喵",
"links": "https://github.com/JJMdzh"
"name": "吉吉喵",
"links": "https://github.com/JJMdzh"
}
],
"settings_ui": "settings.json",