Files
bettergi-scripts-list/repo/js/AotoTeyvatFoodOneDragon/main.js
江紫烟owo 6ce23e594c AotoTeyvatFoodOneDragon-V1.1版本更新 (#2221)
- 修改了用户名获取方式方式,改为了设置界面指定用户名(历史记录与历史收益均受到了影响,但避免了后续出现bug的可能性)
- 修改了部分不合逻辑的地方(如自动拾取触发时机等)
- 修复了一些bug(如坐标比较逻辑中的潜在问题等)
- 优化内存使用和资源释放机制
- 添加了用户名验证
- 添加了指定时间结束运行的功能
2025-10-24 09:26:39 +08:00

2364 lines
99 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(async function () {
// ===== 1. 预处理部分 =====
// 配置读取和变量初始化
let filePath = `assets/`;
const CaiJiPartyName = settings.CaiJiPartyName || "null";
const ZDYparty = settings.ZDYparty || "null";
const userName = settings.userName || "默认账户";
// Windows文件名非法字符列表
const illegalCharacters = /[\\/:*?"<>|]/;
// Windows保留设备名称列表
const reservedNames = [
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
];
// 检查userName是否为空或只有空格
if (!userName || userName.trim() === "") {
log.error(`账户名 "${userName}" 不合法,账户名为空或只包含空格。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName是否以空格开头
else if (userName.startsWith(" ")) {
log.error(`账户名 "${userName}" 不合法,以空格开头。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName是否以空格结尾
else if (userName.endsWith(" ")) {
log.error(`账户名 "${userName}" 不合法,以空格结尾。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName是否以点号结尾
else if (userName.endsWith(".")) {
log.error(`账户名 "${userName}" 不合法,以点号结尾。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName是否包含非法字符
else if (illegalCharacters.test(userName)) {
log.error(`账户名 "${userName}" 不合法,包含非法字符。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName是否是保留设备名称
else if (reservedNames.includes(userName.toUpperCase())) {
log.error(`账户名 "${userName}" 不合法,是保留设备名称。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName长度是否超过255字符
else if (userName.length > 255) {
log.error(`账户名 "${userName}" 不合法,账户名过长。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
// 检查userName长度是否为0
else if (userName.length === 0) {
log.error(`账户名不合法,账户名为空。`);
log.error(`将终止程序,请使用合法的名称`);
await sleep(5000);
return;
}
else {
log.info(`账户名 "${userName}" 合法。`);
}
const cdRecordPath = `record/${userName}_cd.txt`;// 修改CD记录文件路径包含用户名
const ifMonthcard = settings.ifMonthcard;
const ifingredientProcessing = settings.ifingredientProcessing;
const ingredientProcessingFood = settings.ingredientProcessingFood;
const foodCounts = settings.foodCount;
const stove = settings.stove || "蒙德炉子";
const ifCheck = settings.ifCheck || "不查询收益";
const ifendTosafe = settings.ifendTosafe;
let Foods = [];
if (typeof ingredientProcessingFood === 'string' && ingredientProcessingFood.trim()) {
Foods = ingredientProcessingFood.split(/[,;\s]+/)
.map(word => word.trim())
.filter(word => word.length > 0);
}
let foodCount = [];
if (typeof foodCounts === 'string' && foodCounts.trim()) {
foodCount = foodCounts.split(/[,;\s]+/)
.map(word => word.trim())
.filter(word => word.length > 0);
}
// 常量定义
const Pamon = `assets/RecognitionObject/Paimon.png`;
const targetFoods = new Set([
"面粉", "兽肉", "鱼肉", "神秘的肉", "黑麦粉", "奶油", "熏禽肉",
"黄油", "火腿", "糖", "香辛料", "酸奶油", "蟹黄", "果酱",
"奶酪", "培根", "香肠"
]);
// 区域配置
const regions = [
{ enabled: settings.ifMengde, keyword: '蒙德' },
{ enabled: settings.ifLiyue, keyword: '璃月' },
{ enabled: settings.ifDaoqi, keyword: '稻妻' },
{ enabled: settings.ifXumi, keyword: '须弥' },
{ enabled: settings.ifFengdan, keyword: '枫丹' },
{ enabled: settings.ifNata, keyword: '纳塔' },
{ enabled: settings.ifNDKL, keyword: '挪德卡莱' },
{ enabled: settings.ifMine, keyword: '自定义' }
];
// 状态变量
let currentTask = 0; // 当前任务计数器
let firstScan, secondScan, nowStatus, earning, stovePosition;
// ===== 2. 子函数定义部分 =====
// 背包过期物品识别需要在背包界面并且是1920x1080分辨率下使用
async function handleExpiredItems() {
const ifGuoqi = await textOCR("物品过期", 1.5, 0, 0, 870, 280, 170, 40);
if (ifGuoqi.found) {
log.info("检测到过期物品,正在处理...");
await sleep(500);
await click(980, 750); // 点击确认按钮,关闭提示
} else {
log.info("未检测到过期物品");
}
}
// 自动开关门
async function autoSwitchDoor(status) {
try {
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
await sleep(1000);
await keyPress("F2");
await sleep(1000);
let now = await textOCR("", 2, 0, 2, 270, 1000, 150, 45);
if (now.text == status) {
log.debug("当前状态:" + now.text + ",无需调整");
await genshin.returnMainUi();
return "无需调整";
}
await click(385, 1020);//进行权限设置
await sleep(500);
const wantStatus = await textOCR(status, 2, 0, 0, 270, 850, 155, 155);
await sleep(500);
if (wantStatus.found) {
click(wantStatus.x + 15, wantStatus.y + 15);
}
await sleep(500);
const nowStatus = await textOCR("", 2, 0, 2, 270, 1000, 150, 45);
log.debug("当前状态:" + nowStatus.text);
await genshin.returnMainUi();
return now.text;
} catch (error) {
log.error(`调整世界权限出错: ${error.message}`);
}
}
/**
* 获取下一个0点的时间戳
* @returns {string} 返回下一个0点的ISO格式时间字符串
*/
function getNextMidnight() {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return tomorrow.toISOString();
}
/**
* 检查文件是否存在
* @param {string} filePath - 要检查的文件路径
* @returns {Promise<boolean>} 返回Promiseresolve时返回布尔值表示文件是否存在
*/
async function checkFileExists(filePath) {
try {
// 尝试读取文件,如果成功则文件存在
await file.readText(filePath);
return true;
} catch (e) {
// 如果读取失败,则文件可能不存在
return false;
}
}
/**
* 读取CD记录
* @returns {Promise<Object>} 返回包含CD记录的对象键为名称值为时间戳
*/
async function readCDRecords() {
let records = {};
// 使用基础方法检查文件是否存在
const fileExists = await checkFileExists(cdRecordPath);
if (fileExists) {
try {
const content = await file.readText(cdRecordPath);
const lines = content.split('\n');
for (const line of lines) {
if (line.trim()) {
const [name, timestamp] = line.split('::');
records[name] = timestamp;
}
}
} catch (e) {
log.error(`读取CD记录失败: ${e}`);
}
}
return records;
}
/**
* 写入CD记录
* @param {Object} records - 要写入的记录对象,键值对形式
* @returns {Promise<void>} 无返回值的异步函数
*/
async function writeCDRecords(records) {
let content = '';
for (const name in records) {
content += `${name}::${records[name]}\n`;
}
try {
// 尝试创建目录(如果环境支持)
try {
if (typeof file.mkdir === 'function') {
file.mkdir('record');
}
} catch (e) {
// 忽略目录创建错误
}
await file.writeText(cdRecordPath, content);
} catch (e) {
log.error(`写入CD记录失败: ${e}`);
}
}
/**
* 检查路线是否可执行CD是否已刷新
* @param {string} routeName - 路线名称
* @param {Object} cdRecords - CD记录对象键为路线名称值为CD结束时间
* @returns {boolean} 返回true表示路线可执行false表示路线不可执行
*/
function isRouteAvailable(routeName, cdRecords) {
const now = new Date();
// 如果记录中没有该路线,说明是第一次执行,可以执行
if (!cdRecords[routeName]) {
return true;
}
// 检查CD时间是否已过
const cdTime = new Date(cdRecords[routeName]);
return now >= cdTime;
}
// 获取背包物品信息
async function cancanneed() {
/**
* 独立的背包扫描函数(魔改版)
* @param {Object} options - 扫描选项
* @returns {Promise<Array>} 返回物品信息数组 [{name, count}, ...]
*/
async function scanBackpackItems(options = {}) {
// 默认配置
const config = {
// 鼠标按住位置的X坐标用于页面滚动操作
holdX: 1050,
// 鼠标按住位置的Y坐标用于页面滚动操作
holdY: 750,
// 页面滚动距离,控制每次翻页时滑动的距离,数值越小翻页越精细但需要更多次翻页
pageScrollDistance: 300,
// 图像识别延迟(毫秒),在识别物品时的延迟时间,用于平衡性能和准确性
imageDelay: 20,
// 目标计数,设定要识别的目标数量上限
targetCount: 9999,
// 页面切换延迟(毫秒),在切换背包分类页面时的等待时间
pageSwitchDelay: 100,
// 物品识别超时时间(毫秒),单个物品识别的最大等待时间
itemRecognitionTimeout: 300,
// 将传入的options对象属性合并到config中允许外部自定义配置
...options
};
// 材料分类映射表
const materialTypeMap = {
"锻造素材": "5",
"怪物掉落素材": "3",
"一般素材": "5",
"周本素材": "3",
"烹饪食材": "5",
"角色突破素材": "3",
"木材": "5",
"宝石": "3",
"鱼饵鱼类": "5",
"角色天赋素材": "3",
"武器突破素材": "3",
"采集食物": "4",
"料理": "4",
};
// 材料前位定义
const materialPriority = {
"养成道具": 1,
"祝圣精华": 2,
"锻造素材": 1,
"怪物掉落素材": 1,
"采集食物": 1,
"一般素材": 2,
"周本素材": 2,
"料理": 2,
"烹饪食材": 3,
"角色突破素材": 3,
"木材": 4,
"宝石": 4,
"鱼饵鱼类": 5,
"角色天赋素材": 5,
"武器突破素材": 6,
};
// 工具函数
function basename(filePath) {
if (!filePath || typeof filePath !== 'string') return '';
const normalizedPath = filePath.replace(/\\/g, '/');
const lastSlashIndex = normalizedPath.lastIndexOf('/');
return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
}
// 改进的数字替换映射表处理OCR识别误差特别优化了数字7的识别
const numberReplaceMap = {
"O": "0", "o": "0", "Q": "0", "": "0", "D": "0",
"I": "1", "l": "1", "i": "1", "": "1", "一": "1", "|": "1",
"Z": "2", "z": "2", "": "2", "二": "2",
"E": "3", "e": "3", "": "3", "三": "3", "B": "3",
"A": "4", "a": "4", "": "4", "F": "4",
"S": "5", "s": "5", "": "5", "s": "5",
"G": "6", "b": "6", "": "6", "g": "6",
"T": "7", "t": "7", "": "7", "Y": "7", "V": "7", "F": "7", "J": "7", "L": "7", "Z": "7", // 增加更多7的可能误识别字符
"B": "8", "θ": "8", "": "8", "&": "8",
"g": "9", "q": "9", "": "9", "P": "9", "p": "9",
"O": "0", "C": "0", "U": "0"
};
// 核心工具函数
var globalLatestRa = null;
async function recognizeImage(
recognitionObject,
ra,
timeout = 800, // 适中的超时时间
interval = 150, // 适中的检查间隔
useNewScreenshot = false,
iconType = null
) {
let startTime = Date.now();
globalLatestRa = ra;
const originalRa = ra;
let tempRa = null;
try {
while (Date.now() - startTime < timeout) {
let currentRa;
if (useNewScreenshot) {
if (tempRa) {
tempRa.dispose();
}
tempRa = captureGameRegion();
currentRa = tempRa;
globalLatestRa = currentRa;
} else {
currentRa = originalRa;
}
if (currentRa) {
try {
const result = currentRa.find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
return {
isDetected: true,
iconType: iconType,
x: result.x,
y: result.y,
width: result.width,
height: result.height,
ra: globalLatestRa,
usedNewScreenshot: useNewScreenshot
};
}
} catch (error) {
log.error(`${iconType || '未知'}识别异常】: ${error.message}`);
}
}
await sleep(interval);
}
} finally {
if (tempRa && tempRa !== globalLatestRa) {
tempRa.dispose();
}
}
return {
isDetected: false,
iconType: iconType,
x: null,
y: null,
width: null,
height: null,
ra: globalLatestRa,
usedNewScreenshot: useNewScreenshot
};
}
// OCR识别文本数字识别特化
async function recognizeText(ocrRegion, timeout = 800, retryInterval = 20, maxAttempts = 8, maxFailures = 3, ra = null) {
let startTime = Date.now();
let retryCount = 0;
let failureCount = 0;
const frequencyMap = {};
let createdRa = false;
let localRa = ra;
try {
if (!localRa) {
localRa = captureGameRegion();
createdRa = true;
}
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
let ocrObject = null;
let resList = null;
try {
ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.82;
resList = localRa.findMulti(ocrObject);
if (resList.count === 0) {
failureCount++;
if (failureCount >= maxFailures) {
ocrRegion.x += 2;
ocrRegion.width -= 4;
ocrRegion.y += 1;
ocrRegion.height -= 2;
retryInterval += 10;
if (ocrRegion.width <= 12 || ocrRegion.height <= 12) {
return false;
}
}
retryCount++;
await sleep(retryInterval);
continue;
}
let resultText = null;
for (let res of resList) {
let text = res.text;
text = text.replace(/[TtYVJL]/g, "7");
text = text.split('').map(char => numberReplaceMap[char] || char).join('');
text = text.replace(/[^0-9]/g, "");
if (!frequencyMap[text]) {
frequencyMap[text] = 0;
}
frequencyMap[text]++;
if (frequencyMap[text] >= 2 || (text.length > 0 && retryCount >= 3)) {
resultText = text || "?";
break;
}
}
// 先释放资源再返回
if (resultText !== null) {
return resultText;
}
} finally {
// 确保每次循环的资源都被释放
if (ocrObject && typeof ocrObject.dispose === 'function') {
ocrObject.dispose();
}
if (resList && typeof resList.dispose === 'function') {
resList.dispose();
}
}
await sleep(retryInterval);
retryCount++;
}
const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]);
return sortedResults.length === 0 ? "?" : sortedResults[0];
} finally {
if (createdRa && localRa && typeof localRa.dispose === 'function') {
localRa.dispose();
}
}
}
// 通用鼠标拖动函数
async function mouseDrag({
holdMouseX,
holdMouseY,
totalDistance,
stepDistance = 180, // 适中的步长
stepInterval = 15, // 适中的步间隔
waitBefore = 30, // 适中的等待时间
waitAfter = 200, // 适中的等待时间
repeat = 1,
state = { isRunning: true }
}) {
try {
if (holdMouseX !== undefined && holdMouseY !== undefined) {
moveMouseTo(holdMouseX, holdMouseY);
await sleep(waitBefore);
}
leftButtonDown();
await sleep(waitBefore);
for (let r = 0; r < repeat; r++) {
if (!state.isRunning) break;
const steps = Math.ceil(Math.abs(totalDistance) / stepDistance);
const direction = totalDistance > 0 ? 1 : -1;
for (let s = 0; s < steps; s++) {
if (!state.isRunning) break;
const remaining = Math.abs(totalDistance) - s * stepDistance;
const move = Math.min(stepDistance, remaining) * direction;
moveMouseBy(0, move);
await sleep(stepInterval);
}
await sleep(waitAfter);
}
await sleep(waitAfter);
leftButtonUp();
await sleep(waitBefore);
} catch (e) {
log.error(`拖动出错: ${e.message}`);
leftButtonUp();
}
}
// 滑动页面函数
async function scrollPage(totalDistance, stepDistance = 15, delayMs = 10) {
await mouseDrag({
holdMouseX: config.holdX,
holdMouseY: config.holdY,
totalDistance: totalDistance,
stepDistance,
stepInterval: delayMs,
waitBefore: 30,
waitAfter: 200,
repeat: 1
});
}
// 获取材料优先级
function filterMaterialsByPriority(materialsCategory) {
const currentPriority = materialPriority[materialsCategory];
if (currentPriority === undefined) {
throw new Error(`Invalid materialsCategory: ${materialsCategory}`);
}
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 finalFilteredMaterials = [...backPriorityMaterials, materialsCategory];
return finalFilteredMaterials;
}
// 扫描材料 - 优化版本
async function scanMaterials(materialsCategory, materialCategoryMap) {
// 获取当前+后位材料名单
const priorityMaterialNames = [];
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
for (const category of finalFilteredMaterials) {
const materialIconDir = `assets/RecognitionObject/${category}`;
try {
if (file.isFolder && file.isFolder(materialIconDir)) {
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", "");
priorityMaterialNames.push({ category, name });
}
}
} catch (e) {
// 文件夹不存在,跳过
log.debug(`文件夹 ${materialIconDir} 不存在,跳过读取`);
}
}
// 根据材料分类获取对应的材料图片文件夹路径
const materialIconDir = `assets/RecognitionObject/${materialsCategory}`;
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set();
const materialRecognitionObject = {};
// 检查 materialCategoryMap 中当前分类的数组是否为空
const categoryMaterials = materialCategoryMap[materialsCategory] || [];
const shouldScanAllMaterials = categoryMaterials.length === 0;
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", "");
if (typeof pathingMode !== 'undefined' && pathingMode.onlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) {
continue;
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue;
}
materialCategories.push({ name, filePath });
allMaterials.add(name);
materialRecognitionObject[name] = mat;
}
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
const materialInfo = [];
// 扫描参数
const tolerance = 1;
const startX = 117;
const startY = 121;
const OffsetWidth = 146.428;
const columnWidth = 123;
const columnHeight = 680;
const maxColumns = 8;
const pageScrollCount = 35;
// 扫描状态
let hasFoundFirstMaterial = false;
let lastFoundTime = null;
let shouldEndScan = false;
let foundPriorityMaterial = false;
// 扫描背包中的材料
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
const ra = captureGameRegion();
if (!foundPriorityMaterial) {
for (const { category, name } of priorityMaterialNames) {
if (recognizedMaterials.has(name)) {
continue;
}
const filePath = `assets/RecognitionObject/${category}/${name}.png`;
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue;
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1142, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.78; // 适中的阈值
recognitionObject.Use3Channels = true;
const result = ra.find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
foundPriorityMaterial = true;
// log.info(`发现当前或后位材料: ${name},开始全列扫描`);
break;
}
}
}
if (foundPriorityMaterial) {
for (let column = 0; column < maxColumns; column++) {
const scanX0 = startX + column * OffsetWidth;
const scanX = Math.round(scanX0);
for (let i = 0; i < materialCategories.length; i++) {
const { name } = materialCategories[i];
if (recognizedMaterials.has(name)) {
continue;
}
const mat = materialRecognitionObject[name];
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.82;
recognitionObject.Use3Channels = true;
const result = ra.find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
recognizedMaterials.add(name);
moveMouseTo(result.x, result.y);
const ocrRegion = {
x: result.x - tolerance,
y: result.y + 97 - tolerance,
width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance
};
// 增加OCR识别尝试次数和时间提高数字7识别成功率
const ocrResult = await recognizeText(ocrRegion, 1000, 25, 10, 4, ra);
materialInfo.push({ name, count: ocrResult || "?" });
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
lastFoundTime = Date.now();
}
}
await sleep(config.imageDelay);
}
}
}
// 检查是否结束扫描
if (recognizedMaterials.size === allMaterials.size) {
log.info("所有材料均已识别!");
shouldEndScan = true;
break;
}
if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 4000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break;
}
// 检查是否到达最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.75;
const sliderBottomResult = ra.find(sliderBottomRo);
if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!");
shouldEndScan = true;
break;
}
// 滑动到下一页
if (scroll < pageScrollCount) {
await scrollPage(-config.pageScrollDistance, 15, 10);
await sleep(100); // 适中的页面切换延迟
}
}
return materialInfo;
}
// 动态材料分组
function dynamicMaterialGrouping(materialCategoryMap) {
const dynamicMaterialGroups = {};
for (const category in materialCategoryMap) {
const type = materialTypeMap[category];
if (!dynamicMaterialGroups[type]) {
dynamicMaterialGroups[type] = [];
}
dynamicMaterialGroups[type].push(category);
}
for (const type in dynamicMaterialGroups) {
dynamicMaterialGroups[type].sort((a, b) => materialPriority[a] - materialPriority[b]);
}
const sortedGroups = Object.entries(dynamicMaterialGroups)
.map(([type, categories]) => ({ type: parseInt(type), categories }))
.sort((a, b) => a.type - b.type);
return sortedGroups;
}
try {
// 确保材料分类是数组格式
let categories = options.materialCategories;
if (!categories) {
// 如果没有指定分类,则扫描所有分类
categories = Object.keys(materialTypeMap);
}
categories = Array.isArray(categories) ? categories : [categories];
// 创建一个materialCategoryMap
const materialCategoryMap = {};
categories.forEach(category => {
if (materialTypeMap[category]) {
materialCategoryMap[category] = [];
}
});
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
const allMaterialInfo = [];
// 返回主界面
await genshin.returnMainUi();
await sleep(300);
// 打开背包界面
keyPress("B");
await handleExpiredItems();
await sleep(1500);
let cachedFrame = captureGameRegion();
const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/Bagpack.png"), 58, 31, 38, 38);
const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 3000);
if (!backpackResult.isDetected) {
log.warn("未识别到背包图标");
return [];
}
let currentGroupIndex = 0;
let currentCategoryIndex = 0;
let materialsCategory = "";
while (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);
click(menuClickX, 75);
await sleep(config.pageSwitchDelay);
await moveMouseTo(1288, 124);
await sleep(config.pageSwitchDelay);
cachedFrame?.dispose();
cachedFrame = captureGameRegion();
// 识别材料分类
let CategoryObject;
switch (materialsCategory) {
case "锻造素材":
case "一般素材":
case "烹饪食材":
case "木材":
case "鱼饵鱼类":
CategoryObject = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/Materials.png"), 941, 29, 38, 38);
break;
case "采集食物":
case "料理":
CategoryObject = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/Food.png"), 845, 31, 38, 38);
break;
case "怪物掉落素材":
case "周本素材":
case "角色突破素材":
case "宝石":
case "角色天赋素材":
case "武器突破素材":
CategoryObject = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/CultivationItems.png"), 749, 30, 38, 38);
break;
default:
log.error("未知的材料分类");
currentCategoryIndex++;
continue;
}
await sleep(1500);
let CategoryResult = await recognizeImage(CategoryObject, cachedFrame, 5000);
if (!CategoryResult.isDetected) {
log.warn(`首次未识别到材料分类图标: ${materialsCategory},重试中...`);
CategoryResult = await recognizeImage(CategoryObject, cachedFrame, 5000);
if (!CategoryResult.isDetected) {
log.error(`重试后仍未识别到材料分类图标: ${materialsCategory}`);
}
} else {
// log.info(`识别到${materialsCategory} 所在分类。`);
// 重置材料滑条
await moveMouseTo(1288, 124);
await sleep(50);
leftButtonDown();
await sleep(200);
leftButtonUp();
await sleep(100);
// 扫描材料
const materialInfo = await scanMaterials(materialsCategory, materialCategoryMap);
allMaterialInfo.push(...materialInfo);
}
currentCategoryIndex++;
} else {
currentGroupIndex++;
currentCategoryIndex = 0;
}
}
await genshin.returnMainUi();
log.info("扫描流程结束");
cachedFrame?.dispose();
return allMaterialInfo;
} catch (error) {
log.error(`背包扫描出错: ${error.message}`);
return [];
} finally {
await genshin.returnMainUi();
}
}
// const items = await scanBackpackItems();
// // 输出结果
// items.forEach(item => {
// log.info(`物品名称: ${item.name}, 数量: ${item.count}`);
// });
// 扫描特定分类
let materialCategories;
switch (ifCheck) {
case "查询所有收益":
materialCategories = ["一般素材", "烹饪食材"];
break;
case "查询食材加工收益":
materialCategories = ["烹饪食材"];
break;
default:
materialCategories = ["一般素材", "烹饪食材"];
}
const items = await scanBackpackItems({
materialCategories: materialCategories
});
return items;
}
/**
* 快速计算物品数量差值,处理识别失败情况
* @param {Array} scan1 第一次扫描结果
* @param {Array} scan2 第二次扫描结果
* @param {Array} itemNames 物品名称数组
* @returns {Object} {物品名称: 数量差值或"识别失败"}
*/
async function getItemDifferences(scan1, scan2, itemNames) {
const diff = {};
// 创建查找映射
const createMap = (scan) => {
const map = {};
scan.forEach(item => {
// 保留原始数量值,不转换为数字
map[item.name] = item.count;
});
return map;
};
const map1 = createMap(scan1);
const map2 = createMap(scan2);
// 如果itemNames为空则获取所有物品名称
const itemsToCheck = itemNames && itemNames.length > 0
? itemNames
: [...new Set([...Object.keys(map1), ...Object.keys(map2)])];
// 计算每个物品的差值
itemsToCheck.forEach(name => {
const count1 = map1[name] || "0";
const count2 = map2[name] || "0";
// 检查是否有任意一次识别失败
if (count1 === "?" || count2 === "?") {
diff[name] = "识别失败";
} else {
// 将数量转换为数字进行计算
const num1 = parseInt(count1) || 0;
const num2 = parseInt(count2) || 0;
diff[name] = num2 - num1;
}
});
/**
* 将收益数据保存到txt文件中只保留最近七次记录
* @param {Object} diff - 收益差异对象
*/
async function saveEarningsToFile(diff) {
const earningsFilePath = `record/${userName}_earnings.txt`;
try {
// 创建目录(如果不存在)
try {
if (typeof file.mkdir === 'function') {
file.mkdir('record');
}
} catch (e) {
// 忽略目录创建错误
}
// 读取现有记录
let records = [];
try {
const content = await file.readText(earningsFilePath);
if (content.trim()) {
records = content.trim().split('\n\n').filter(record => record.trim() !== '');
}
} catch (e) {
// 文件不存在或读取失败,使用空数组
}
// 创建新记录
const now = new Date();
const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
let newRecord = `=== ${formattedDate} ===\n(收益结果受多因素影响,可能会不准确,本结果仅供参考)\n`;
let hasChanges = false;
Object.keys(diff).forEach(item => {
const change = diff[item];
if (change === "识别失败") {
// 记录识别失败的项目
newRecord += `${item}: 识别失败\n`;
hasChanges = true; // 确保即使只有识别失败的项目也会显示"有变化"
} else {
if (change !== 0) {
newRecord += `${item}: ${change > 0 ? '+' : ''}${change}\n`;
hasChanges = true;
}
}
});
if (!hasChanges) {
newRecord += "无收益变化\n";
}
newRecord += "=====================\n";
// 添加新记录到开头
records.unshift(newRecord);
// 只保留最近7条记录
if (records.length > 7) {
records = records.slice(0, 7);
}
// 写入文件
const contentToWrite = records.join('\n\n');
await file.writeText(earningsFilePath, contentToWrite);
log.info(`收益已保存至 ${earningsFilePath},共 ${records.length} 条记录`);
log.warn("收益记录仅供参考!");
return newRecord;
} catch (error) {
log.error(`保存收益记录失败: ${error.message}`);
}
}
const newRecord = await saveEarningsToFile(diff);
log.info("\n" + newRecord);
return newRecord;
}
/**
* 文字OCR识别封装函数测试中未封装完成后续会优化逻辑
* @param text 要识别的文字,默认为"空参数"
* @param timeout 超时时间单位为秒默认为10秒
* @param afterBehavior 点击模式0表示不点击1表示点击识别到文字的位置2表示输出模式默认为0
* @param debugmodel 调试代码0表示输入判断模式1表示输出位置信息2表示输出判断模式默认为0
* @param x OCR识别区域的起始X坐标默认为0
* @param y OCR识别区域的起始Y坐标默认为0
* @param w OCR识别区域的宽度默认为1920
* @param h OCR识别区域的高度默认为1080
* @returns 包含识别结果的对象,包括识别的文字、坐标和是否找到的结果
*/
async function textOCR(text = "空参数", timeout = 10, afterBehavior = 0, debugmodel = 0, x = 0, y = 0, w = 1920, h = 1080) {
const startTime = new Date();
var Outcheak = 0
for (var ii = 0; ii < 10; ii++) {
// 获取一张截图
var captureRegion = captureGameRegion();
var res1
var res2
var conuntcottimecot = 1;
var conuntcottimecomp = 1;
// 对整个区域进行 OCR
var resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, w, h));
//log.info("OCR 全区域识别结果数量 {len}", resList.count);
if (resList.count !== 0) {
for (let i = 0; i < resList.count; i++) { // 遍历的是 C# 的 List 对象,所以要用 count而不是 length
let res = resList[i];
res1 = res.text
conuntcottimecomp++;
if (res.text.includes(text) && debugmodel == 3) { return result = { text: res.text, x: res.x, y: res.y, found: true }; }
if (res.text.includes(text) && debugmodel !== 2) {
conuntcottimecot++;
if (debugmodel === 1 & x === 0 & y === 0) { log.info("全图代码位置:({x},{y},{h},{w})", res.x - 10, res.y - 10, res.width + 10, res.Height + 10); }
if (afterBehavior === 1) { await sleep(1000); click(res.x, res.y); } else { if (debugmodel === 1 & x === 0 & y === 0) { log.info("点击模式:关") } }
if (afterBehavior === 2) { await sleep(100); keyPress("F"); } else { if (debugmodel === 1 & x === 0 & y === 0) { log.info("F模式:关"); } }
if (conuntcottimecot >= conuntcottimecomp / 2) { return result = { text: res.text, x: res.x, y: res.y, found: true }; } else { return result = { found: false }; }
}
if (debugmodel === 2) {
if (res1 === res2) { conuntcottimecot++; res2 = res1; }
//log.info("输出模式:全图代码位置:({x},{y},{h},{w},{string})", res.x-10, res.y-10, res.width+10, res.Height+10, res.text);
if (Outcheak === 1) { if (conuntcottimecot >= conuntcottimecomp / 2) { return result = { text: res.text, x: res.x, y: res.y, found: true }; } else { return result = { found: false }; } }
}
}
}
const NowTime = new Date();
if ((NowTime - startTime) > timeout * 1000) { if (debugmodel === 2) { if (resList.count === 0) { return result = { found: false }; } else { Outcheak = 1; ii = 2; } } else { Outcheak = 0; if (debugmodel === 1 & x === 0 & y === 0) { log.info(`${timeout}秒超时退出,"${text}"未找到`) }; return result = { found: false }; } }
else { ii = 2; if (debugmodel === 1 & x === 0 & y === 0) { log.info(`"${text}"识别中……`); } }
await sleep(100);
}
}
/**
* 封装函数,执行图片识别及点击操作(测试中,未封装完成,后续会优化逻辑)
* @param {string} imagefilePath - 模板图片路径
* @param {number} timeout - 超时时间(秒)
* @param {number} afterBehavior - 识别后行为(0:无,1:点击,2:按F键)
* @param {number} debugmodel - 调试模式(0:关闭,1:详细日志)
* @param {number} xa - 识别区域X坐标
* @param {number} ya - 识别区域Y坐标
* @param {number} wa - 识别区域宽度
* @param {number} ha - 识别区域高度
* @param {boolean} clickCenter - 是否点击目标中心
* @param {number} clickOffsetX - 点击位置X轴偏移量
* @param {number} clickOffsetY - 点击位置Y轴偏移量
* @param {number} tt - 匹配阈值(0-1)
*/
async function imageRecognitionEnhanced(
imagefilePath = "空参数",
timeout = 10,
afterBehavior = 0,
debugmodel = 0,
xa = 0,
ya = 0,
wa = 1920,
ha = 1080,
clickCenter = false,
clickOffsetX = 0,
clickOffsetY = 0,
tt = 0.8
) {
// 参数验证
if (xa + wa > 1920 || ya + ha > 1080) {
log.info("图片区域超出屏幕范围");
return { found: false, error: "区域超出屏幕范围" };
}
const startTime = Date.now();
let captureRegion = null;
let result = { found: false };
try {
// 读取模板图像
const templateImage = file.ReadImageMatSync(imagefilePath);
if (!templateImage) {
throw new Error("无法读取模板图像");
}
const Imagidentify = RecognitionObject.TemplateMatch(templateImage, true);
if (tt !== 0.8) {
Imagidentify.Threshold = tt;
Imagidentify.InitTemplate();
}
// 循环尝试识别
for (let attempt = 0; attempt < 10; attempt++) {
if (Date.now() - startTime > timeout * 1000) {
if (debugmodel === 1) {
log.info(`${timeout}秒超时退出,未找到图片`);
}
break;
}
captureRegion = captureGameRegion();
if (!captureRegion) {
await sleep(200);
continue;
}
try {
const croppedRegion = captureRegion.DeriveCrop(xa, ya, wa, ha);
const res = croppedRegion.Find(Imagidentify);
if (res.isEmpty()) {
if (debugmodel === 1) {
log.info("识别图片中...");
}
} else {
// 计算基准点击位置(目标的左上角)
let clickX = res.x + xa;
let clickY = res.y + ya;
// 如果要求点击中心,计算中心点坐标
if (clickCenter) {
clickX += Math.floor(res.width / 2);
clickY += Math.floor(res.height / 2);
}
// 应用自定义偏移量
clickX += clickOffsetX;
clickY += clickOffsetY;
if (debugmodel === 1) {
log.info("计算后点击位置:({x},{y})", clickX, clickY);
}
// 执行识别后行为
if (afterBehavior === 1) {
await sleep(1000);
click(clickX, clickY);
} else if (afterBehavior === 2) {
await sleep(1000);
keyPress("F");
}
result = {
x: clickX,
y: clickY,
w: res.width,
h: res.height,
found: true
};
break;
}
} finally {
if (captureRegion) {
captureRegion.dispose();
captureRegion = null;
}
}
await sleep(200);
}
} catch (error) {
log.info(`图像识别错误: ${error.message}`);
result.error = error.message;
}
return result;
}
/**
* 伪造日志信息的异步函数,用于模拟脚本或地图追踪任务的开始与结束日志。
* @param {string} name - 日志中显示的任务名称(如脚本名、地图追踪文件名)
* @param {boolean} isJs - 标识是否为 JS 脚本日志true 表示 JS 脚本false 表示地图追踪)
* @param {boolean} isStart - 标识是开始还是结束日志true 表示开始false 表示结束)
* @param {number} duration - 持续时间(单位:毫秒),仅在结束日志时有意义
*
* 注意事项:
* - 必须使用 await 调用此函数以避免 V8 引擎相关错误;
* - duration 参数只在伪造“结束”日志时起作用,且主要用于日志展示;
*/
async function fakeLog(name, isJs, isStart, duration) {
await sleep(10);
const currentTime = Date.now();
// 参数检查
if (typeof name !== 'string') {
log.error("参数 'name' 必须是字符串类型!");
return;
}
if (typeof isJs !== 'boolean') {
log.error("参数 'isJs' 必须是布尔型!");
return;
}
if (typeof isStart !== 'boolean') {
log.error("参数 'isStart' 必须是布尔型!");
return;
}
if (typeof currentTime !== 'number' || !Number.isInteger(currentTime)) {
log.error("参数 'currentTime' 必须是整数!");
return;
}
if (typeof duration !== 'number' || !Number.isInteger(duration)) {
log.error("参数 'duration' 必须是整数!");
return;
}
// 将 currentTime 转换为 Date 对象并格式化为 HH:mm:ss.sss
const date = new Date(currentTime);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
const formattedTime = `${hours}:${minutes}:${seconds}.${milliseconds}`;
// 将 duration 转换为分钟和秒,并保留三位小数
const durationInSeconds = duration / 1000; // 转换为秒
const durationMinutes = Math.floor(durationInSeconds / 60);
const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数
// 使用四个独立的 if 语句处理四种情况
if (isJs && isStart) {
// 处理 isJs = true 且 isStart = true 的情况
const logMessage = `正在伪造js开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行JS脚本: "${name}"`;
log.debug(logMessage);
}
if (isJs && !isStart) {
// 处理 isJs = true 且 isStart = false 的情况
const logMessage = `正在伪造js结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
if (!isJs && isStart) {
// 处理 isJs = false 且 isStart = true 的情况
const logMessage = `正在伪造地图追踪开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行地图追踪任务: "${name}"`;
log.debug(logMessage);
}
if (!isJs && !isStart) {
// 处理 isJs = false 且 isStart = false 的情况
const logMessage = `正在伪造地图追踪结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
}
/**
* 获取JSON文件中最后一个对象的xy坐标
* @param {string} filePath - JSON文件路径
* @returns {Object|null} 包含x和y坐标的对象失败时返回null
*/
async function getLastPositionCoords(filePath) {
try {
// 使用异步文件读取函数
const content = await file.readText(filePath);
// 为了减少内存使用在使用完content后尽快释放
let positions;
let lastPosition;
let result;
try {
const jsonData = JSON.parse(content);
// 检查数据结构
if (!jsonData || typeof jsonData !== 'object') {
throw new Error('无效的JSON数据结构');
}
positions = jsonData.positions;
if (!positions || !Array.isArray(positions) || positions.length === 0) {
throw new Error('positions数组为空或不存在');
}
// 获取最后一个点位
lastPosition = positions[positions.length - 1];
// 检查坐标字段
if (typeof lastPosition.x === 'undefined' || typeof lastPosition.y === 'undefined') {
throw new Error('最后一个点位缺少x或y坐标');
}
result = {
x: lastPosition.x,
y: lastPosition.y,
positionId: lastPosition.id || null,
totalPositions: positions.length,
index: positions.length - 1
};
} finally {
// 显式清理对象引用,帮助垃圾回收
positions = null;
lastPosition = null;
}
return result;
} catch (error) {
log.error(`处理文件 ${filePath} 时出错:`, error.message);
return null;
}
}
/**
* 比较两个坐标是否近似相等
* @param {Object} coord1 - 第一个坐标对象 {x, y}
* @param {Object} coord2 - 第二个坐标对象 {x, y}
* @param {number} tolerance - 容差范围默认1.0
* @returns {boolean} 是否近似相等
*/
function areCoordinatesApproximate(coord1, coord2, tolerance = 1.0) {
if (!coord1 || !coord2) return false;
const xDiff = Math.abs(coord1.x - coord2.X);
const yDiff = Math.abs(coord1.y - coord2.Y);
return xDiff <= tolerance && yDiff <= tolerance;
}
/**
* 路线筛选函数
* 根据国家模式和路线名称来判断是否执行该路线
* @param {string} fName - 路线名称
* @param {string} countryMode - 国家模式,决定路线筛选规则
* @returns {boolean} 返回true表示执行该路线false表示不执行
*/
async function routeSelection(fName, countryMode) {
// 如果国家模式是"不运行"则不执行任何路线
if (countryMode === "不运行") {
return false;
}
switch (countryMode) {
case "全跑模式":
return true;
case "高效&螃蟹模式":
return fName.includes("高效") || fName.includes("螃蟹");
case "高效模式":
return fName.includes("高效");
case "螃蟹模式":
return fName.includes("螃蟹");
default:
return false;
}
}
/**
* 异步执行一组地图追踪路线文件,根据国家模式筛选并按顺序运行符合条件的路线。
* 包括冷却时间检查、路径执行、坐标校验、重试机制以及月卡点击等功能。
* @param {string} filePathDir - 地图追踪文件所在的目录路径
* @param {string} countryMode - 当前国家的运行模式(如“只运行”、“不运行”等)
* @param {string} countryName - 国家名称,用于日志输出标识
* @param {Object} totalCount - 总任务数对象,包含 totalKeywordCount 属性表示总关键词数量
*/
async function runPathGroups(filePathDir, countryMode, countryName, totalCount) {
try {
if (countryName == "自定义") {
await switchPartyIfNeeded(ZDYparty);
}
if (countryMode != "不运行") {
log.info(`正在加载${countryName}的地图追踪文件,运行模式: ${countryMode}`);
await sleep(150);
}
// 读取CD记录
const cdRecords = await readCDRecords();
let updatedRecords = { ...cdRecords };
// 读取文件夹中的文件名并处理
const filePaths = file.readPathSync(filePathDir);
const jsonFilePaths = [];
for (const filePath of filePaths) {
if (filePath.endsWith('.json')) {
jsonFilePaths.push(filePath);
}
}
//添加验证,目录为空就结束运行
if (jsonFilePaths.length === 0) {
throw new Error('目录中没有找到JSON文件停止运行');//抛出异常
}
log.info("地图追踪文件加载完成!");
await sleep(150);
/**
* 异步运行一组路线文件,依次执行路径脚本并进行相关逻辑判断与重试机制。
* @param { string[] } files - 要执行的路线文件名数组(含路径)
*/
async function runFiles(files) {
// 启用自动拾取
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
for (const fileName of files) {
try {
// 提取路线名称(不含.json后缀
const fName = fileName.split('\\').pop().split('/').pop().replace('.json', '');
// 判断是否选择该路线执行(根据路线名和国家模式)
if (!await routeSelection(fName, countryMode)) {
continue;
}
const routeName = fName;
currentTask++;
// 检查冷却时间是否已刷新,未刷新则跳过该路线
if (!isRouteAvailable(routeName, cdRecords)) {
log.info(`路线 ${routeName} CD未刷新跳过`);
continue;
}
let retryCount = 0;
const maxRetries = 3; // 最大重试次数,避免无限循环
// 循环尝试执行路径直到成功或达到最大重试次数
while (retryCount < maxRetries) {
await fakeLog(routeName, false, true, 0);
log.info(`当前任务:${routeName} 为第${currentTask}/${totalCount.totalKeywordCount}`);
// 运行指定的路径文件
await pathingScript.runFile(fileName);
await sleep(300);
let position1, position2;
try {
// 获取路径结束位置坐标及当前游戏内小地图坐标
position1 = await getLastPositionCoords(fileName);
position2 = genshin.getPositionFromMap();
} catch (error) {
position2 = { X: 0, Y: 0 }
}
log.debug(`当前任务末位坐标: X=${position1.x}, Y=${position1.y}`);
log.debug(`当前小地图坐标: X=${position2.X}, Y=${position2.Y}`);
// 判断两个坐标的偏差是否在允许范围内
const ifDeviation = areCoordinatesApproximate(position1, position2, 25);
await sleep(10);
if (ifDeviation) {
// 坐标匹配更新CD记录并跳出重试循环
updatedRecords[routeName] = getNextMidnight();
await writeCDRecords(updatedRecords);
break; // 跳出重试循环,继续下一个文件
} else {
await fakeLog(routeName, false, false, 0);
// 坐标偏差较大,进入重试流程
log.info(`坐标偏差,第${retryCount + 1}次重试 ${routeName}`);
// 防被抓策略延迟等待
await exponentialBackoffRetry(preventBeingcaught, 3, 500);
retryCount++;
if (retryCount >= maxRetries) {
log.info(`路线 ${routeName} 重试${maxRetries}次后仍失败,跳过`);
}
}
}
await fakeLog(routeName, false, false, 0);
const ifExit = checkExitTime(settings.exitTime); // 检查是否已到退出时间
if (!ifExit) {
// 计算需要等待的时间
const waitTime = calculateWaitTime(settings.exitTime);
if (waitTime > 0) {
log.warn(`距离退出时间还有约${Math.floor(waitTime / 1000 / 60)}${Math.floor((waitTime / 1000) % 60)}`);
await genshin.tpToStatueOfTheSeven(); // 回神像等别被肘死了再怪作者……
await sleep(waitTime + 10000); // 等待时间差+10秒
}
}
try {
// 若启用月卡功能,则点击领取月卡奖励
if (ifMonthcard) {
await clickMonthcard();//月卡点击
} else {
log.debug("月卡点击功能未开启");
}
} catch (error) {
log.error(`月卡功能发生错误: ${error}`);
}
if (!ifExit) {
log.warn("已到退出时间,将退出程序");
await genshin.returnMainUi();
throw new Error("Exit time reached."); // 抛出异常
}
} catch (e) {
if (e.message == "A task was canceled." || e.message == "Exit time reached.") {
throw e; // 继续抛出错误
} else {
log.error(`处理文件 ${fileName} 时出错: ${e.message},已跳过该路径`);
continue; // 继续执行下一个路径
}
}
}
// 关闭自动拾取,防止误触炉子
dispatcher.ClearAllTriggers();
// 获取炉子位置坐标并与当前位置比较,决定是否需要执行食材加工
let position4;
try {
position4 = genshin.getPositionFromMap();
} catch (error) {
position4 = { X: 0, Y: 0 }
}
if (stovePosition && ifingredientProcessing) {
log.debug(`炉子坐标: X=${stovePosition.x}, Y=${stovePosition.y}`);
log.debug(`当前小地图坐标: X=${position4.X}, Y=${position4.Y}`);
const ifDeviation1 = areCoordinatesApproximate(stovePosition, position4, 15);
await sleep(10);
if (ifDeviation1) {
log.debug("距离上一次加工过近,不执行食材加工");
return;
}
}
// 执行食材加工
await ingredientProcessing();
await sleep(10);
}
// 执行地图追踪文件
await runFiles(jsonFilePaths);
} catch (e) {
if (e.message == "A task was canceled." || e.message == "Exit time reached.") {
throw e;
}
log.error(`路径组执行失败: ${e.message}`);
}
}
/**
* 计算距离退出时间的等待时间
* @param {string} timeSetting - 时间设置字符串,格式为 "HH:MM" 或 "HHMM"
* @returns {number} 需要等待的毫秒数
*/
function calculateWaitTime(timeSetting) {
if (!timeSetting || !/^\d{1,2}[:]\d{2}$/.test(timeSetting)) return 0;
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentSecond = now.getSeconds();
// 解析设置的时间(支持中英文冒号)
const [targetHour, targetMinute] = timeSetting.split(/[:]/).map(Number);
// 验证时间有效性
if (isNaN(targetHour) || isNaN(targetMinute) ||
targetHour < 0 || targetHour > 23 ||
targetMinute < 0 || targetMinute > 59) {
return 0;
}
// 计算当前时间和目标时间的总秒数
const currentTotalSeconds = currentHour * 3600 + currentMinute * 60 + currentSecond;
const targetTotalSeconds = targetHour * 3600 + targetMinute * 60;
// 计算需要等待的秒数
let secondsToWait;
if (currentTotalSeconds < targetTotalSeconds) {
// 同一天内
secondsToWait = targetTotalSeconds - currentTotalSeconds;
} else {
// 跨天情况(目标时间是第二天)
secondsToWait = (24 * 3600 - currentTotalSeconds) + targetTotalSeconds;
}
return secondsToWait * 1000; // 转换为毫秒
}
/**
* 检查当前时间是否接近禁止运行的时间点提前10分钟停止
* @param {string} timeSetting - 时间设置字符串,格式为 "HH:MM" 或 "HHMM",如 "04:30" 或 "0430" 表示凌晨4点30分
* @returns {boolean} 如果当前时间距离禁止运行时间点不足10分钟返回false否则返回true
*/
function checkExitTime(timeSetting) {
if (!timeSetting || !/^\d{1,2}[:]\d{2}$/.test(timeSetting)) return true;
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// 解析设置的时间(支持中英文冒号,会有小笨猪写中文冒号吗?)
const [targetHour, targetMinute] = timeSetting.split(/[:]/).map(Number);
// 验证时间有效性
if (isNaN(targetHour) || isNaN(targetMinute) ||
targetHour < 0 || targetHour > 23 ||
targetMinute < 0 || targetMinute > 59) {
return true;
}
// 计算当前时间与目标时间的分钟差
const currentTotalMinutes = currentHour * 60 + currentMinute;
const targetTotalMinutes = targetHour * 60 + targetMinute;
// 计算距离目标时间的分钟数(考虑跨天情况)
let minutesToTarget;
if (currentTotalMinutes <= targetTotalMinutes) {
// 同一天内
minutesToTarget = targetTotalMinutes - currentTotalMinutes;
} else {
// 跨天情况(目标时间是第二天)
minutesToTarget = (24 * 60 - currentTotalMinutes) + targetTotalMinutes;
}
// 如果距离目标时间不足10分钟返回false表示应该退出
if (minutesToTarget <= 10 && minutesToTarget >= 0) {
log.warn(`当前时间 ${currentHour}:${currentMinute.toString().padStart(2, '0')} 距离禁止运行时间 ${targetHour}:${targetMinute.toString().padStart(2, '0')} 不足10分钟脚本将退出`);
return false;
}
// 如果当前时间正好在目标时间,也退出
if (currentHour === targetHour && currentMinute === targetMinute) {
log.warn(`当前时间 ${currentHour}:${currentMinute.toString().padStart(2, '0')} 为禁止运行时间,脚本将退出`);
return false;
}
return true;
}
/**
* 逃逸函数
* 通过模拟点击操作来防止被活动捕捉,执行一系列点击动作后返回主界面
* @returns {Promise<void>} 无返回值的异步函数
*/
async function preventBeingcaught() {
// 解决剧情弹出页观景点以及其他奇奇怪怪的页面理论上都有办法逃出。长剧情无法完全退出时在运行下一脚本前会触发bgi剧情接管
// 因为某些问题频繁交互的场景下无法解决
const res1 = await imageRecognitionEnhanced(Pamon, 1.5, 0, 0, 39, 31, 38, 38);
if (res1.found) {
return;
}
for (let i = 0; i < 3; i++) {
await click(1370, 800); // 尝试点击剧情对话位置
await sleep(300);
await keyPress("ESCAPE"); // 尝试点击ESC键返回主页面
await sleep(300);
await click(1370, 800);
await sleep(300);
}
const res2 = await imageRecognitionEnhanced(Pamon, 1.5, 0, 0, 39, 31, 38, 38);
if (res2.found) {
return;
} else {
throw new Error("多次尝试未能返回主页面");
}
}
/**
* 指数退避重试函数
* 当操作失败时,按照指数退避算法进行重试,重试间隔逐渐增加
* @param {Function} operation - 需要执行的操作函数应返回Promise
* @param {number} maxRetries - 最大重试次数默认为5次
* @param {number} baseDelay - 基础延迟时间(毫秒)默认为500ms
* @returns {Promise} 返回操作函数的执行结果
*/
async function exponentialBackoffRetry(operation, maxRetries = 5, baseDelay = 500) {
for (let i = 0; i < maxRetries; i++) {
try {
await click(975, 985); // 点击“复苏”(如果有),先排除全体阵亡的可能性
await sleep(100);
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, i); // 指数退避
log.info(`操作失败,${delay}ms 后进行第${i + 1}次重试`);
await sleep(delay);
}
}
}
// 切换队伍
async function switchPartyIfNeeded(partyName) {
if (partyName == "null") { log.warn("未填写队伍名称,将以当前队伍执行脚本!"); return; }
try {
if (!await genshin.switchParty(partyName)) {
log.info("切换队伍失败,前往七天神像重试");
await genshin.tpToStatueOfTheSeven();
await genshin.switchParty(partyName);
}
} catch {
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
// notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`);
await genshin.returnMainUi();
}
}
// 寻路函数
async function AutoPath(locationName) {
try {
let filePath = `assets/${locationName}.json`;
await pathingScript.runFile(filePath);
} catch (error) {
log.error(`执行 ${locationName} 路径时发生错误`);
}
}
// 月卡点击功能
async function clickMonthcard() {
// 获取当前时间
const now = new Date();
// 计算下一个4点的时间
let next4am = new Date(now);
next4am.setHours(4, 0, 0, 0);
if (now >= next4am) {
// 如果已经过了今天4点则取明天4点
next4am.setDate(next4am.getDate() + 1);
}
// 计算距离下一个4点的毫秒数
const diffMs = next4am - now;
const diffMinutes = diffMs / 1000 / 60;
// 如果距离4点小于10分钟
if (diffMinutes < 10) {
log.info(`距离4点还有${diffMinutes.toFixed(2)}分钟,等待领取月卡...`);
await genshin.tpToStatueOfTheSeven();
// 持续等待直到过了4点
while (true) {
const current = new Date();
if (current >= next4am) {
await sleep(10000);//四点之后等十秒再点击
for (let i = 0; i < 5; i++) {
click(960, 740);
await sleep(100);
}
break;
}
await sleep(1000);
}
const res = await imageRecognitionEnhanced(Pamon, 1.5, 0, 0, 39, 31, 38, 38);
if (res.found) {
log.info("已领取月卡");
} else {
for (let i = 0; i < 3; i++) {
click(960, 740);
await sleep(100);
}
log.info("已领取月卡");
}
}
}
/**
* 食材加工主函数,用于自动前往指定地点进行食材或料理的加工制作
*
* 该函数会根据 Foods 和 foodCount 数组中的食材名称和数量,依次查找并制作对应的料理/食材
* 支持两种类型:普通料理(需滚动查找)和调味品类食材(直接在“食材加工”界面查找)
*
* @returns {Promise<void>} 无返回值,执行完所有加工流程后退出
*/
async function ingredientProcessing() {
if (!ifingredientProcessing) { return; }
if (Foods.length == 0) { log.error("未选择要加工的料理/食材"); return; }
if (Foods.length != foodCount.length) { log.error("请检查料理与对应的数量是否一致!"); return; }
log.info(`正在前往${stove}进行食材加工`);
await AutoPath(stove);
const res1 = await textOCR("烹饪", 5, 0, 0, 1150, 460, 155, 155);
if (res1.found) {
await sleep(10);
keyDown("VK_MENU");
await sleep(500);
click(res1.x + 15, res1.y + 15);
} else {
log.warn("烹饪按钮未找到,正在寻找……");
let attempts = 0;
const maxAttempts = 3;
let foundInRetry = false;
while (attempts < maxAttempts) {
log.info(`${attempts + 1}次尝试寻找烹饪按钮`);
keyPress("W");
const res2 = await textOCR("烹饪", 5, 0, 0, 1150, 460, 155, 155);
if (res2.found) {
await sleep(10);
keyDown("VK_MENU");
await sleep(500);
click(res2.x + 15, res2.y + 15);
foundInRetry = true;
break;
} else {
attempts++;
await sleep(500);
}
}
if (!foundInRetry) {
log.error("多次未找到烹饪按钮,放弃寻找");
return;
}
}
await sleep(800);
keyUp("VK_MENU");
await sleep(1000);
for (let i = 0; i < Foods.length; i++) {
if (targetFoods.has(Foods[i])) {//调味品就点到对应页面
const res3 = await textOCR("食材加工", 1, 0, 0, 140, 30, 115, 30);
if (!res3.found) {
await sleep(500);
click(1010, 55);
await sleep(500);
}
const res = await textOCR("全部领取", 1, 0, 0, 195, 1000, 120, 40);
if (res.found) {
click(res.x, res.y);
await sleep(800);
click(960, 750);
await sleep(500);
}
const res1 = await textOCR(Foods[i], 1, 0, 3, 116, 116, 1165, 505);
if (res1.found) {
log.info(`${Foods[i]}已找到`);
await click(res1.x + 50, res1.y - 60);
} else {
await sleep(500);
let ra = captureGameRegion();
try {
const ocrResult = ra.findMulti(RecognitionObject.ocr(115, 115, 1150, 502));
const foodItems = []; // 存储找到的相关项目
// 收集所有包含"分钟"或"秒"的项目
for (let j = 0; j < ocrResult.count; ++j) {
if (ocrResult[j].text.endsWith("分钟") || ocrResult[j].text.endsWith("秒")) {
foodItems.push({
index: j,
x: ocrResult[j].x,
y: ocrResult[j].y
});
}
}
log.debug("检查到的正在加工食材的数量:" + foodItems.length);
// 依次筛选这些项目
for (const item of foodItems) {
// 点击该项目
click(item.x, item.y);
await sleep(150);
click(item.x, item.y);
await sleep(800);
let res2 = await textOCR("", 0.5, 0, 2, 1320, 100, 150, 50);
log.debug("当前项目:" + res2.text);
log.debug(item.x + "," + item.y);
await sleep(1000);
if (res2.text === Foods[i]) {
ra?.dispose();
res1.found = true;
log.info(`从正在加工的食材中找到了:${Foods[i]}`);
break;
}
}
if (!res1.found) {
log.error(`未找到目标食材: ${Foods[i]}`);
ra?.dispose();
continue;
}
} finally {
ra?.dispose();
}
}
await sleep(1000);
click(1700, 1020);// 制作
await sleep(800);
click(960, 460);
await sleep(800);
inputText(foodCount[i]);
log.info(`尝试制作${Foods[i]} ${foodCount[i]}`);
log.warn("由于受到队列和背包食材数量限制,实际制作数量与上述数量可能不一致!");
await sleep(800);
click(1190, 755);
await sleep(800);
} else {
const res3 = await textOCR("料理制作", 1, 0, 0, 140, 30, 115, 30);
if (!res3.found) {
await sleep(500);
click(910, 55);
await sleep(500);
}
click(145, 1015);// 筛选
await sleep(800);
click(195, 1015);// 重置
await sleep(800);
click(500, 1020);// 确认筛选
await sleep(800);
//滚轮预操作
await moveMouseTo(1287, 131);
await sleep(100);
await leftButtonDown();
await sleep(100);
await moveMouseTo(1287, 161);
let YOffset = 0; // Y轴偏移量根据需要调整
const maxRetries = 20; // 最大重试次数
let retries = 0; // 当前重试次数
while (retries < maxRetries) {
const res2 = await textOCR(Foods[i], 1, 0, 3, 116, 116, 1165, 880);
if (res2.found) {
await leftButtonUp();
await sleep(500);
await click(res2.x + 50, res2.y - 60);
await sleep(1000);
await sleep(1000);
click(1700, 1020);// 制作
await sleep(1000);
await textOCR("自动烹饪", 5, 1, 0, 725, 1000, 130, 45);
await sleep(800);
click(960, 460);
await sleep(800);
inputText(foodCount[i]);
await sleep(800);
click(1190, 755);
await sleep(2500); // 等待烹饪完成
keyPress("ESCAPE")
await sleep(500);
keyPress("ESCAPE")
await sleep(1500);
break;
} else {
retries++; // 重试次数加1
//滚轮操作
YOffset += 50;
await sleep(500);
if (retries === maxRetries || 161 + YOffset > 1080) {
await leftButtonUp();
await sleep(100);
await moveMouseTo(1287, 131);
await sleep(800);
leftButtonClick();
log.error(`料理/食材:${Foods[i]} 未找到!请检查料理名称是否正确!`);
continue;
}
await moveMouseTo(1287, 161 + YOffset);
await sleep(300);
}
}
}
}
await genshin.returnMainUi();
}
/**
* 筛选要执行的区域
*
* 该函数用于读取指定路径下的文件夹,并根据 regions 配置筛选出需要执行的区域。
* 它会检查每个 region 是否启用,并尝试在读取到的文件夹中查找匹配关键字的文件夹。
* 最终返回一个包含路径、执行模式和关键字的对象数组。
*
* @returns {Promise<Array<{path: string, mode: string, keyword: string}>>}
* 返回一个包含要执行区域信息的数组,每个元素包括:
* - path: 匹配的文件夹路径
* - mode: 执行模式(来自 region.enabled
* - keyword: 匹配的关键字(来自 region.keyword
*/
async function executeRegions() {
const executedCountries = [];
try {
// log.debug(`尝试读取路径: ${filePath}`);
// 读取assets目录下的所有文件夹
let allPaths = [];
try {
const res = file.readPathSync(filePath);
const result = Array.from(res);
// log.debug(`readPathSync 返回类型: ${typeof result}`);
// log.debug(`readPathSync 返回内容: ${JSON.stringify(result)}`);
allPaths = Array.isArray(result) ? result : [];
} catch (e) {
log.error("无法读取assets目录: " + e.message);
return;
}
// log.debug(`解析后的路径数组长度: ${allPaths.length}`);
// log.debug(`路径数组内容: ${JSON.stringify(allPaths)}`);
// 过滤出有效的文件夹
const folders = [];
for (const path of allPaths) {
if (typeof path !== 'string') {
log.debug(`跳过非字符串路径: ${typeof path} = ${path}`);
continue;
}
// log.debug(`检查路径: ${path}`);
try {
let isFolder = file.isFolder(path);
if (isFolder) {
folders.push(path);
// log.debug(`识别为有效文件夹: ${path}`);
} else {
log.debug(`路径不是有效文件夹: ${path}`);
}
} catch (e) {
log.debug(`无法读取路径 ${path}: ${e.message}`);
// 忽略无法读取的路径
}
}
log.debug("读取到的所有文件夹: " + JSON.stringify(folders));
// 创建文件夹数组副本,用于后续比对
let remainingFolders = [...folders];
// 筛选启用的区域
for (const region of regions) {
if (!region.enabled) {
region.enabled = "不采集";
}
if (region.enabled != "不采集") {
let targetFolderIndex = -1;
let targetFolder = null;
// 在剩余的文件夹中查找匹配项
targetFolderIndex = remainingFolders.findIndex(folder => {
if (typeof folder !== 'string') return false;
// 获取文件夹名称
const pathParts = folder.split(/[\/\\]/); // 同时支持正斜杠和反斜杠
const folderName = pathParts[pathParts.length - 1].toLowerCase();
// log.debug(`检查文件夹: ${folderName}, 关键字: ${region.keyword.toLowerCase()}`);
return folderName.includes(region.keyword.toLowerCase());
});
if (targetFolderIndex !== -1) {
targetFolder = remainingFolders[targetFolderIndex];
// 从剩余文件夹数组中移除已匹配的文件夹,减轻后续比对压力
remainingFolders.splice(targetFolderIndex, 1);
executedCountries.push({
path: targetFolder,
mode: region.enabled,
keyword: region.keyword
});
} else {
log.warn(`未找到包含"${region.keyword}"的文件夹`);
}
}
}
} catch (error) {
log.error(`读取文件夹时发生错误: ${error.message}`);
}
return executedCountries;
}
/**
* 统计指定区域目录中JSON文件的数量以及包含关键词的JSON文件数量
* @param {Array} executeRegions - 区域数组每个区域对象应包含path属性表示目录路径
* @param {Array} keywords - 关键词数组用于筛选文件名中包含特定关键词的JSON文件
* @returns {Object} 统计结果对象包含总JSON文件数、总关键词匹配数以及各区域详细统计信息
*/
function countJsonFiles(executeRegions) {
const results = {
totalKeywordCount: 0,
regions: []
};
executeRegions.forEach(region => {
const folderPath = region.path;
let keywordCount = 0;
let keywords;
switch (region.mode) {
case "高效&螃蟹模式":
keywords = ['螃蟹', '高效'];
break;
case "不采集":
return; // 在 forEach 中跳过当前区域
case "全跑模式":
keywords = [];
break;
case "螃蟹模式":
keywords = ['螃蟹'];
break;
case "高效模式":
keywords = ['高效'];
break;
default:
// 处理其他未知模式,可以设置为空数组或默认值
keywords = [region.mode]; // 或者根据需求调整
break;
}
try {
// 使用 readPathSync 读取目录内容
const items = file.readPathSync(folderPath);
for (const item of items) {
// 检查是否为 JSON 文件
if (item.endsWith('.json')) {
// 提取纯文件名(不含路径和扩展名)
const fileName = item.split('\\').pop().split('/').pop().replace('.json', '');
// 检查是否满足关键词条件
if (keywords.length === 0 || keywords.some(keyword => fileName.includes(keyword))) {
keywordCount++;
}
}
}
} catch (error) {
log.warn(`无法读取目录 ${folderPath}:`, error.message);
}
// 更新统计结果
results.totalKeywordCount += keywordCount; // 总关键词匹配数
results.regions.push({ // 区域统计结果
path: folderPath,
keywordCount
});
});
return results;
}
// ===== 3. 主函数执行部分 =====
try {
//伪造js开始记录
await fakeLog("AotoTeyvatFoodOneDragon", true, true, 0);
// 初始化游戏设置
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
await switchPartyIfNeeded(CaiJiPartyName);
// 获取炉子位置
if (ifingredientProcessing) {
try {
stovePosition = await getLastPositionCoords(`assets/${stove}.json`);
} catch (error) {
log.warn(`获取炉子坐标失败: ${error.message}`);
stovePosition = null;
}
}
const countrys = await executeRegions(); // 筛选要运行的国家
const jsonCount = countJsonFiles(countrys); // 统计JSON文件数
if (ifCheck != "不查询收益") {
nowStatus = await autoSwitchDoor("不允许加入");
firstScan = await cancanneed();
}
for (const country of countrys) {
await runPathGroups(country.path, country.mode, country.keyword, jsonCount);
await sleep(10);
}
//执行完毕返回主页面
await genshin.returnMainUi();
log.info("地图追踪文件执行完毕!");
// 收益查询后处理
if (ifCheck !== "不查询收益") {
secondScan = await cancanneed();
// 根据查询类型确定要检查的物品列表
let itemsToCheck = [];
if (ifCheck === "查询所有收益") {
itemsToCheck = [];
} else if (ifCheck === "查询食材加工收益") {
itemsToCheck = Foods;
}
earning = await getItemDifferences(firstScan, secondScan, itemsToCheck);
if (nowStatus != "无需调整") { await autoSwitchDoor(nowStatus); }
}
if (ifendTosafe) {
log.info("正在前往指定的七天神像……");
await genshin.tpToStatueOfTheSeven();
} else {
log.debug("未开启返回七天神像功能,不返回七天神像!");
}
log.info("终于跑完了!下班下班!!我要吃桃子🍑!!!");
//伪造js结束记录
await fakeLog("AotoTeyvatFoodOneDragon", true, false, 0);
if (settings.notify) {
notification.Send("AotoTeyvatFoodOneDragon运行结束\n" + earning);
}
} catch (error) {
// 伪造错误结束记录
await fakeLog("AotoTeyvatFoodOneDragon", true, false, 0);
if (error.message != "Exit time reached.") {
notification.error("任务中断:" + error.message);
}
log.error("任务中断:" + error.message);
return;
}
})();