diff --git a/repo/js/CD-Aware-AutoGather/README.md b/repo/js/CD-Aware-AutoGather/README.md new file mode 100644 index 00000000..97800017 --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/README.md @@ -0,0 +1,56 @@ +# 使用前准备 + +**双击运行脚本所在目录下的`SymLink.bat`文件,以创建符号链接。** + +此操作只需要一次。运行后,脚本下的`pathing`文件夹将指向Better GI的地图追踪文件夹`User\AutoPathing`,你通过Better GI新增或删除地图追踪任务后,脚本这边看到的也是修改后的。 + +# 运行模式 + +必须先完成`使用前准备`里的操作 + +## 1. 扫描文件夹更新可选材料列表 + +扫描脚本`pathing`目录(等价于你在Better GI里订阅的地图追踪任务目录)下的`地方特产`、`矿物`、`食材与炼金`内的材料,并自动匹配材料的刷新时间。 + +扫描完成后,将自动更新脚本可用的配置菜单。此时再次打开右键的`修改JS脚本自定义配置`,将看到新增了多个配置项,其中包含刚刚扫描到的材料目录。 + +![Preview](preview.png) + +如果你订阅了很多地图追踪任务,那么扫描结果也会比较多,选项列表也会比较长,但不影响脚本运行。 + +只有你新增或者删除了地图追踪任务的订阅时,才需要运行此模式。 + +## 2. 采集选中的材料 + +此模式下有这些选项可以配置: + +| 选项 | 说明 | +| ---- | ---- | +| 设置要使用的队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 | +| 停止运行时间 | 超过此时间后,停止后续的任务(会等待已经运行中的json路线结束)。 | +| 我肝的账号不止一个 | 如果你有多个账号,可以选中此选项,选中后将分账号维护对应的材料刷新时间。 | +| `↓` 地方特产\稻妻\绯樱绣球 | 根据你订阅的路径追踪任务数量,这里将会显示相应个数的选择框。
勾选后将执行你选中的条目的采集任务。
Tip: `↓`符号是在提示你应该勾选文本下面的选择框 | + +运行此模式后,将按照你勾选的条目,执行相应的采集任务。每执行完一条json路线后,将会计算它的下次刷新时间并写入`record`文件夹下的记录文件。下次运行脚本时,未刷新的路线将自动跳过。 + +可以同时勾选多种材料,会逐个进行采集。 + +如果不同的采集任务需要不同队伍,那请在调度器配置组里添加多次本脚本,然后分别设置不同的采集物和采集队伍。 + +支持使用配置组`更多功能`——`日志分析`分析运行记录(参考了[mno](https://github.com/Bedrockx)大佬的写法)。 + +![log](log_analysis.png) + +## 3. 清除运行记录(重置材料刷新时间) + +此模式下,相关的选项只有`我肝的账号不止一个`和以`↓`开头的任务名,作用同上文。 + +运行此模式后,将重置你选中的任务相应材料的刷新时间。 + +如果你需要删除全部运行记录,可以直接删除脚本`record`文件夹下,以账号为名称的文件夹内的文件。 + +--- + +***想要参与改进此脚本?*** + +脚本目录下有个`脚本思路.txt`文件,先看看它可以快速理解我开发时的思路,易于快速上手。 diff --git a/repo/js/CD-Aware-AutoGather/SymLink.bat b/repo/js/CD-Aware-AutoGather/SymLink.bat new file mode 100644 index 00000000..757fb86a --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/SymLink.bat @@ -0,0 +1,12 @@ +@REM @echo off +set "target1=..\..\AutoPathing" +set "target2=..\..\pathing" + +if exist "%target1%" ( + mklink /j pathing "%target1%" +) else if exist "%target2%" ( + mklink /j pathing "%target2%" +) else ( + echo ERROR: Can't find folder "%target1%" or "%target2%" + pause +) diff --git a/repo/js/CD-Aware-AutoGather/log_analysis.png b/repo/js/CD-Aware-AutoGather/log_analysis.png new file mode 100644 index 00000000..30573d21 Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/log_analysis.png differ diff --git a/repo/js/CD-Aware-AutoGather/main.js b/repo/js/CD-Aware-AutoGather/main.js new file mode 100644 index 00000000..3c44c284 --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/main.js @@ -0,0 +1,592 @@ +const CooldownType = { + Unknown: "未配置刷新机制", + Every1DayMidnight: "每1天的0点", + Every2DaysMidnight: "每2天的0点", + Every3DaysMidnight: "每3天的0点", + Daily4AM: "每天凌晨4点", + Every12Hours: "12小时刷新", + Every24Hours: "24小时刷新", + Every46Hours: "46小时刷新", +}; + +const CooldownDataBase = { + 沉玉仙茗: CooldownType.Every24Hours, + 发光髓: CooldownType.Every12Hours, + 蝴蝶翅膀: CooldownType.Every12Hours, + 晶核: CooldownType.Every12Hours, + 鳗肉: CooldownType.Every12Hours, + 螃蟹: CooldownType.Every12Hours, + 禽肉: CooldownType.Every12Hours, + 青蛙: CooldownType.Every12Hours, + 鳅鳅宝玉: CooldownType.Every12Hours, + 神秘的肉: CooldownType.Every12Hours, + 兽肉: CooldownType.Every12Hours, + 蜥蜴尾巴: CooldownType.Every12Hours, + 鱼肉: CooldownType.Every12Hours, + 白萝卜: CooldownType.Every1DayMidnight, + 薄荷: CooldownType.Every1DayMidnight, + 澄晶实: CooldownType.Every1DayMidnight, + 墩墩桃: CooldownType.Every1DayMidnight, + 海草: CooldownType.Every1DayMidnight, + 红果果菇: CooldownType.Every1DayMidnight, + 胡萝卜: CooldownType.Every1DayMidnight, + 金鱼草: CooldownType.Every1DayMidnight, + 堇瓜: CooldownType.Every1DayMidnight, + 烬芯花: CooldownType.Every1DayMidnight, + 久雨莲: CooldownType.Every1DayMidnight, + 颗粒果: CooldownType.Every1DayMidnight, + 苦种: CooldownType.Every1DayMidnight, + 莲蓬: CooldownType.Every1DayMidnight, + 烈焰花花蕊: CooldownType.Every1DayMidnight, + 马尾: CooldownType.Every1DayMidnight, + 蘑菇: CooldownType.Every1DayMidnight, + 茉洁草: CooldownType.Every1DayMidnight, + 鸟蛋: CooldownType.Every1DayMidnight, + 泡泡桔: CooldownType.Every1DayMidnight, + 苹果: CooldownType.Every1DayMidnight, + 日落果: CooldownType.Every1DayMidnight, + 树莓: CooldownType.Every1DayMidnight, + 松果: CooldownType.Every1DayMidnight, + 松茸: CooldownType.Every1DayMidnight, + 甜甜花: CooldownType.Every1DayMidnight, + 汐藻: CooldownType.Every1DayMidnight, + 香辛果: CooldownType.Every1DayMidnight, + 星蕈: CooldownType.Every1DayMidnight, + 须弥蔷薇: CooldownType.Every1DayMidnight, + 枣椰: CooldownType.Every1DayMidnight, + 竹笋: CooldownType.Every1DayMidnight, + 烛伞蘑菇: CooldownType.Every1DayMidnight, + 沉玉仙茗: CooldownType.Every24Hours, + 晶蝶: CooldownType.Daily4AM, + + 铁块: CooldownType.Every1DayMidnight, + 白铁块: CooldownType.Every2DaysMidnight, + 电气水晶: CooldownType.Every2DaysMidnight, + 星银矿石: CooldownType.Every2DaysMidnight, + 萃凝晶: CooldownType.Every3DaysMidnight, + 水晶块: CooldownType.Every3DaysMidnight, + 紫晶块: CooldownType.Every3DaysMidnight, + 奇异的龙牙: CooldownType.Every46Hours, + 冰雾花: CooldownType.Every46Hours, + 烈焰花: CooldownType.Every46Hours, + 地方特产: CooldownType.Every46Hours, +}; + +const settingFile = "settings.json"; +const baseTime = getBaseTime(); + +const baseTimeStr = baseTime.toISOString(); +const timeOffset = Date.parse(baseTimeStr) - Date.parse(baseTimeStr.slice(0, -1)); // 计算时区偏移量 +const timeOffsetStr = offsetToTimezone(timeOffset); +let stopTime = null; + +class ReachStopTime extends Error { + constructor(message) { + super(message); + this.name = "ReachStopTime"; + } +} + +(async function () { + if (! file.IsFolder("pathing")) { + let batFile = "SymLink.bat"; + try { + file.readTextSync(`Ayaka-Main-${Math.random()}.txt`); + } catch (error) { + const err_msg = error.toString(); + const match = err_msg.match(/'([^']+)'/); + const fullPath = match ? match[1] : null; + const folderPath = fullPath ? fullPath.replace(/\\[^\\]+$/, '') : null; + if (folderPath) { + batFile = `${folderPath}\\${batFile}`; + } + } + + log.error("{0}文件夹不存在,请双击运行下列位置的脚本以创建文件夹链接\n{1}", "pathing", batFile); + return; + } + const runMode = settings.runMode; + + const scriptName = getScriptItselfName(); + // 结束真正由BGI产生的那次开始记录 + startTime = fakeLogCore(scriptName, true); + + log.info("当前运行模式:{0}", runMode); + if (runMode === "扫描文件夹更新可选材料列表") { + await runScanMode(); + } else if (runMode === "采集选中的材料") { + await runGatherMode(); + } else if (runMode === "清除运行记录(重置材料刷新时间)") { + await runClearMode(); + } else { + log.warn("未选择运行模式或运行模式无效: {0}\n这可能是你的首次运行,将为你执行{1}模式", runMode, "扫描文件夹更新可选材料列表"); + await sleep(3000); + await runScanMode(); + } + // 重新开始一条记录,与BGI产生的结束记录配对 + fakeLogCore(scriptName, true, startTime); +})(); + +// 扫描文件夹更新可选材料列表 +async function runScanMode() { + // 1. 扫描所有最底层路径 + const focusFolders = ["地方特产", "矿物", "食材与炼金"]; + const pathList = focusFolders.flatMap(fd => getLeafFolders(`pathing/${fd}`)); + + // 2. 读取配置模板 + const templateText = file.readTextSync("settings.template.json"); + let config = JSON.parse(templateText); + + // 3. 处理每个路径 + let count = 0; + for (const path of pathList) { + const info = getCooldownInfoFromPath(path); + if (info.coolType !== CooldownType.Unknown) { + config.push({ + name: info.name, + label: "⬇️ " + info.label, + type: "checkbox" + }); + count += 1; + } else { + log.warn("路径{0}未找到对应的刷新机制,跳过", path); + } + } + // 4. 写入新的配置(格式化输出) + file.writeTextSync(settingFile, JSON.stringify(config, null, 2)); + log.info("共{0}组有效路线,请在脚本配置中勾选需要采集的材料", count); + +} + + +// 采集选中的材料 +async function runGatherMode() { + const selectedMaterials = getSelectedMaterials(); + + if (selectedMaterials.length === 0) { + log.error("未选择任何材料,请在脚本配置中勾选所需项目"); + return; + } + if (settings.stopAtTime) { + stopTime = calcStopTime(settings.stopAtTime); + log.info("脚本已被配置为达到{0}后停止运行", strftime(stopTime, true)); + } + + log.info("共{0}组材料路线待执行:", selectedMaterials.length); + for (const item of selectedMaterials) { + const info = getCooldownInfoFromPath(item.label); + log.info(` - {0} (${info.coolType})`, item.label || item.name); + } + + let account = await getCurrentAccount(); + log.info("为{0}采集材料并管理CD", account); + + if (settings.partyName) { + try { + if (!(await genshin.switchParty(settings.partyName))) { + log.info("切换队伍失败,前往七天神像重试"); + await genshin.tpToStatueOfTheSeven(); + await sleep(1000); + await genshin.switchParty(settings.partyName); + } + } catch { + log.error("队伍切换失败,可能处于联机模式或其他不可切换状态"); + await genshin.returnMainUi(); + } + } + + dispatcher.addTimer(new RealtimeTimer("AutoPick")); + // 可在此处继续处理 selectedMaterials 列表 + try { + for (const pathTask of selectedMaterials) { + await runPathTaskIfCooldownExpired(account, pathTask); + } + } catch (e) { + if (e instanceof ReachStopTime) { + log.info("达到设置的停止时间 {0},终止运行", strftime(stopTime, true)); + } else { + throw e; + } + } +} + + +// 清除运行记录(重置材料刷新时间) +async function runClearMode() { + const selectedMaterials = getSelectedMaterials(); + + if (selectedMaterials.length === 0) { + log.error("未选择任何材料,请在脚本配置中勾选所需项目"); + } + const resetTime = strftime(baseTime); + let account = await getCurrentAccount(); + for (const pathTask of selectedMaterials) { + const jsonFiles = filterFilesInTaskDir(pathTask); + const recordFile = getRecordFilePath(account, pathTask); + const lines = jsonFiles.map((filePath) => { + return `${basename(filePath)}\t${resetTime}`; + }); + const content = lines.join("\n"); + file.writeTextSync(recordFile, content); + log.info("已重置{0}的刷新时间", pathTask.label); + } + log.info("已重置{0}组刷新时间。如需重置所有材料刷新时间,请直接删除record目录下对应账号的文件夹", selectedMaterials.length); +} + +function getRecordFilePath(account, pathTask) { + const taskName = pathTask.name.replace(/^OPT_/, ""); + return `record/${account}/${taskName}.txt`; +} + +function filterFilesInTaskDir(pathTask, ext=".json") { + const taskDir = pathTask.label; + const allFilesRaw = file.ReadPathSync("pathing\\" + taskDir); + const extFiles = []; + + for (const filePath of allFilesRaw) { + if (filePath.endsWith(ext)) { + extFiles.push(filePath); + } + } + + return extFiles; +} + + +async function runPathTaskIfCooldownExpired(account, pathTask) { + const recordFile = getRecordFilePath(account, pathTask); + const jsonFiles = filterFilesInTaskDir(pathTask); + + log.info("{0}共有{1}条路线", pathTask.label, jsonFiles.length); + + // 2. 读取记录文件(路径 -> 时间) + const recordMap = {}; + try { + const text = file.readTextSync(recordFile); + for (const line of text.split("\n")) { + const [p, t] = line.trim().split("\t"); + if (p && t) { + recordMap[p] = new Date(t); + } + } + } catch (error) { + log.debug(`记录文件{0}不存在或格式错误`, recordFile); + } + + // 3. 检查哪些 json 文件已过刷新时间 + for (let i = 0; i < jsonFiles.length; i++) { + const jsonPath = jsonFiles[i]; + const fileName = basename(jsonPath); + const lastTime = recordMap[fileName] || baseTime; + const pathName = fileName.split(".")[0]; + const progress = `[${i + 1}/${jsonFiles.length}]`; + + if (stopTime && Date.now() >= stopTime) { + throw new ReachStopTime("达到设置的停止时间,终止运行"); + } + + if (Date.now() > lastTime) { + let pathStart = addFakePathLog(fileName); + log.info(`${progress}{0}: 开始执行`, pathName); + + let pathStartTime = new Date(); + try { + await pathingScript.runFile(jsonPath); + } catch (error) { + log.error(`${progress}{0}: 文件不存在或执行失败: {1}`, pathName, error.toString()); + addFakePathLog(fileName, pathStart); + continue; // 跳过当前任务 + } + + // 更新记录 + if (new Date() - pathStartTime > 5000) { + recordMap[fileName] = calculateNextRunTime(new Date(), jsonPath); + const lines = []; + + for (const [p, t] of Object.entries(recordMap)) { + lines.push(`${p}\t${strftime(t)}`); + } + const content = lines.join("\n"); + file.writeTextSync(recordFile, content); + log.info(`${progress}{0}: 已完成,下次刷新: ${strftime(recordMap[fileName], true)}`, pathName); + } else { + log.info(`${progress}{0}: 执行时间过短,不更新记录`, pathName); + } + addFakePathLog(fileName, pathStart); + } else { + log.info(`${progress}{0}: 已跳过 (${strftime(recordMap[fileName], true)}刷新)`, pathName); + } + } +} + +/** + * 根据路径逐级查找最匹配的物品,返回去除前缀的路径、标准化名称、刷新时间 + * @param {string} fullPath - 单个完整路径(包含公共前缀) + * @returns {{ label: string, name: string, coolType: string }} + */ +function getCooldownInfoFromPath(fullPath) { + const parts = fullPath.split(/[\\/]/); // 支持 \ 或 / 分隔符 + let cooldown = CooldownType.Unknown; + let cleanPart = ""; + + for (const part of parts) { + cleanPart = part.split("@")[0]; // 去除 @ 后缀 + + if (CooldownDataBase.hasOwnProperty(cleanPart)) { + cooldown = CooldownDataBase[cleanPart]; + break; + } + } + + const label = parts.slice(1).join("\\"); // 去除公共前缀 + const name = "OPT_" + label.replace(/[^\u4e00-\u9fa5\w]/g, "_"); // 添加前缀并格式化名称 + + return { + label, + name, + coolType: cooldown, + }; +} + +function calculateNextRunTime(base, fullPath) { + const {coolType} = getCooldownInfoFromPath(fullPath); + let nextTime = baseTime; + + switch (coolType) { + case CooldownType.Every1DayMidnight: { + const next = new Date(base.getTime() + 1 * 24 * 60 * 60 * 1000); + next.setHours(0, 0, 0, 0); + nextTime = next; + break; + } + + case CooldownType.Every2DaysMidnight: { + const next = new Date(base.getTime() + 2 * 24 * 60 * 60 * 1000); + next.setHours(0, 0, 0, 0); + nextTime = next; + break; + } + + case CooldownType.Every3DaysMidnight: { + const next = new Date(base.getTime() + 3 * 24 * 60 * 60 * 1000); + next.setHours(0, 0, 0, 0); + nextTime = next; + break; + } + + case CooldownType.Daily4AM: { + const next = new Date(base); + next.setHours(4, 0, 0, 0); + if (base.getHours() >= 4) { + // 如果已过今天凌晨4点,则设为明天的4点 + next.setDate(next.getDate() + 1); + } + nextTime = next; + break; + } + + case CooldownType.Every12Hours: { + nextTime = new Date(base.getTime() + 12 * 60 * 60 * 1000); + break; + } + + case CooldownType.Every24Hours: { + nextTime = new Date(base.getTime() + 24 * 60 * 60 * 1000); + break; + } + + case CooldownType.Every46Hours: { + nextTime = new Date(base.getTime() + 46 * 60 * 60 * 1000); + break; + } + + default: + throw new Error(`未识别的冷却类型: ${coolType}`); + } + + return nextTime; +} + + +/** + * 获取指定路径下所有最底层的文件夹(即不包含任何子文件夹的文件夹) + * @param {string} folderPath - 要遍历的根文件夹路径 + * @param {string[]} result - 用于收集最底层文件夹路径的数组 + * @returns {Promise} 所有最底层文件夹的路径 + */ +function getLeafFolders(folderPath, result = []) { + const filesInSubFolder = file.ReadPathSync(folderPath); + let hasSubFolder = false; + + for (const filePath of filesInSubFolder) { + if (file.IsFolder(filePath)) { + hasSubFolder = true; + // 递归查找子文件夹 + getLeafFolders(filePath, result); + } + } + + // 如果没有发现任何子文件夹,则当前为最底层文件夹 + if (!hasSubFolder) { + result.push(folderPath); + } + + return result; +} + +async function getCurrentAccount() { + let account = "默认账号"; + + if (settings.iHaveMultipleAccounts) { + // 打开背包避免界面背景干扰 + await genshin.returnMainUi(); + keyPress("B"); + await sleep(1000); + + const region = captureGameRegion(); + const ocrResults = RecognitionObject.ocr(region.width * 0.75, region.height * 0.75, region.width * 0.25, region.height * 0.25); + const resList = region.findMulti(ocrResults); + + for (let i = 0; i < resList.count; i++) { + const text = resList[i].text; + if (text.includes("UID")) { + const match = text.match(/\d+/); + if (match) { + account = match[0]; + } + break; + } + } + + if (account === "默认账号") { + log.error("未能提取到UID"); + } + } + + return account; +} + +function getSelectedMaterials() { + const configText = file.readTextSync(settingFile); + const config = JSON.parse(configText); // 配置数组 + + const selectedMaterials = []; + + for (const entry of config) { + if ( + entry.name && + entry.name.startsWith("OPT_") && + entry.type === "checkbox" + ) { + if (settings[entry.name] === true) { + entry.label = entry.label.split(" ")[1]; // 去除⬇️指示 + selectedMaterials.push(entry); + } + } + } + + return selectedMaterials; +} + + +// Happy Birthday +function getBaseTime() { + const now = new Date(); + const year = now.getFullYear() - 18; + return new Date(year, 8, 28, 0, 0, 0); // 9月是month=8(0起始) +} + + +function strftime(dateObj, shortFormat = false) { + const timestamp = dateObj.getTime() + timeOffset; + const newDate = new Date(timestamp); + let s = newDate.toISOString(); + s = s.replace("Z", timeOffsetStr); + + if (shortFormat) { + // 截取出 MM-DD HH:MM:SS + const [datePart, timePart] = s.split("T"); + const [year, month, day] = datePart.split("-"); + const time = timePart.split(".")[0]; // 去掉毫秒部分 + s = `${month}-${day} ${time}`; + } + + return s; +} + +function basename(filePath) { + const lastSlashIndex = filePath.lastIndexOf('\\'); + return filePath.substring(lastSlashIndex + 1); +} + +function offsetToTimezone(offsetMs) { + const totalMinutes = offsetMs / (1000 * 60); + const sign = totalMinutes >= 0 ? "+" : "-"; + const absMinutes = Math.abs(totalMinutes); + const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0"); + const minutes = String(absMinutes % 60).padStart(2, "0"); + return `${sign}${hours}:${minutes}`; +} + +function calcStopTime(timeStr) { + const match = timeStr.match(/\b\d{2}[::]\d{2}\b/); // 匹配 HH:mm + if (!match) { + return new Date(0xFFFFFFFF * 1000); // 不停止 + } + + const [hour, minute] = match[0].split(":").map(Number); + const now = new Date(); + const next = new Date(now); + + next.setHours(hour, minute, 0, 0); + if (next <= now) { + next.setDate(next.getDate() + 1); + } + + return next; +} + +function getScriptItselfName() { + const content = file.readTextSync("manifest.json"); + const manifest = JSON.parse(content); + return manifest.name; +} + +// 参考了 mno 大佬的函数 + +function fakeLogCore(name, isJs = true, dateIn = null) { + const isStart = (isJs === (dateIn !== null)); + const lastRun = isJs ? new Date() : dateIn; + const task = isJs ? "JS脚本" : "地图追踪任务"; + let logMessage = ""; + let logTime = new Date(); + if (isJs && isStart) { + logTime = dateIn; + } + + const logTimeWithOffset = new Date(logTime.getTime() + timeOffset); + const formattedTime = logTimeWithOffset.toISOString().split("T")[1].replace("Z", ""); + + if (isStart) { + logMessage = `正在伪造开始的日志记录\n\n` + + `[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` + + `------------------------------\n\n` + + `[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` + + `→ 开始执行${task}: "${name}"`; + } else { + const durationInSeconds = (logTime.getTime() - lastRun.getTime()) / 1000; + const durationMinutes = Math.floor(durationInSeconds / 60); + const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数 + logMessage = `正在伪造结束的日志记录\n\n` + + `[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` + + `→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}分${durationSeconds}秒\n\n` + + `[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` + + `------------------------------`; + } + log.debug(logMessage); + return logTime; +} + +function addFakePathLog(name, lastRun = null) { + return fakeLogCore(name, false, lastRun); +} diff --git a/repo/js/CD-Aware-AutoGather/manifest.json b/repo/js/CD-Aware-AutoGather/manifest.json new file mode 100644 index 00000000..3dce6425 --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/manifest.json @@ -0,0 +1,15 @@ +{ + "manifest_version": 1, + "name": "带CD管理的自动采集", + "version": "1.0", + "bgi_version": "0.45.0", + "description": "自动同步你通过BetterGI订阅的地图追踪任务,执行采集任务,并管理材料刷新时间(支持多账号)。\n首次运行前请先简单阅读说明,推荐在线版 https://github.com/babalae/bettergi-scripts-list/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件", + "authors": [ + { + "name": "Ayaka-Main", + "link": "https://github.com/Patrick-Ze" + } + ], + "settings_ui": "settings.json", + "main": "main.js" +} \ No newline at end of file diff --git a/repo/js/CD-Aware-AutoGather/preview.png b/repo/js/CD-Aware-AutoGather/preview.png new file mode 100644 index 00000000..1a648cdc Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/preview.png differ diff --git a/repo/js/CD-Aware-AutoGather/settings.json b/repo/js/CD-Aware-AutoGather/settings.json new file mode 100644 index 00000000..ee15553b --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/settings.json @@ -0,0 +1,10 @@ +[ + { + "name": "runMode", + "type": "select", + "label": "首次运行前请先简单阅读说明,推荐在线版\n https://github.com/babalae/bettergi-scripts-list\n/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件", + "options": [ + "扫描文件夹更新可选材料列表" + ] + } +] \ No newline at end of file diff --git a/repo/js/CD-Aware-AutoGather/settings.template.json b/repo/js/CD-Aware-AutoGather/settings.template.json new file mode 100644 index 00000000..5b6012f3 --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/settings.template.json @@ -0,0 +1,27 @@ +[ + { + "name": "runMode", + "type": "select", + "label": "运行模式", + "options": [ + "扫描文件夹更新可选材料列表", + "采集选中的材料", + "清除运行记录(重置材料刷新时间)" + ] + }, + { + "name": "partyName", + "type": "input-text", + "label": "设置要使用的队伍名称(留空则不进行切换)" + }, + { + "name": "stopAtTime", + "type": "input-text", + "label": "停止运行时间(HH:mm,24小时制。例如09:28为上午9点28分)" + }, + { + "name": "iHaveMultipleAccounts", + "type": "checkbox", + "label": "我肝的账号不止一个(选中后将分账号维护对应的材料刷新时间)" + } +] \ No newline at end of file diff --git a/repo/js/CD-Aware-AutoGather/脚本思路.txt b/repo/js/CD-Aware-AutoGather/脚本思路.txt new file mode 100644 index 00000000..3b4bc18a --- /dev/null +++ b/repo/js/CD-Aware-AutoGather/脚本思路.txt @@ -0,0 +1,44 @@ +提供一个bat文件 +1. 创建符号链接,指向路径追踪文件夹 + +js脚本逻辑: +1. 设置文件完全由js脚本生成(可以提交一个生成版本进库作为参考) +2. 提供3种运行模式 + a 扫描文件夹更新可用配置 + b 采集选中的材料 + c 清除运行记录(重置刷新时间) + +可行性探索: +1. 当配置选项新增可选物品时:获取该项目的值会得到undefined(等同于未配置) +2. 当已选中物品从配置选项中删除时:获取该项目的值会得到undefined,但会保存在脚本组的配置里。如果再加回这个选项,会获得之前设置的值。不存在于当前settings.json文件的字段不会自动从脚本组配置中删除 +3. 设置属性里不支持特殊字符 +多账户支持:单账户时记录保存到`默认账号`文件夹,多账户时根据UID创建对应的记录文件夹 + +索引文件列属性: +只记录采集物名称以及对应CD,不记录完整路径。这样的话只要新的路径追踪是符合文件夹结构的,也能自动支持。 +工作时,基于文件夹路径,从前到后全词匹配路径的每个部分,直到找到对应的项目 + +a 扫描模式 +1. 提示可以运行bat脚本或者手动创建符号链接 +2. 遍历追踪文件夹内的所有子文件夹 +3. 基于子文件夹的相对路径,查找索引文件,对于那些在索引中的条目:更新settings.json,创建 options 可选列表,并记录所有选项 +4. settings.json中还需要提供的配置项: + - 运行模式 + - 队伍名称(如果不同的采集物需要使用不同队伍,那要求用户重复添加多个JS运行项) + - 终止运行时间 + - 要采集的物品列表 + - 我肝的账号不止一个 +5. 如果用户添加的文件夹太多,设置项也会很多。可以建议用户适当删除一些 + +b 采集模式: +1. 根据用户选中的采集物,枚举对应文件夹下的路径追踪文件。逐个处理完所有选项 +2. 根据子文件夹的相对路径,查找索引文件,得知其刷新模式 +3. 对于每个追踪文件,循环执行: + a. 查询运行记录,获知刷新时间。如果查询不到,视为未运行过 + b. 如果当前时间大于刷新时间,则执行采集 + c. 执行采集后,计算下次刷新时间并更新运行记录 + +c 清除模式 +1. 根据用户选中的采集物,获取其对应的运行记录 +2. 重置运行记录中的刷新时间为绫华生日 + diff --git a/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json b/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json index a0ddb458..f5e5f15f 100644 --- a/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json +++ b/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json @@ -25,8 +25,8 @@ "x": 3701.55, "y": 3836.57, "type": "target", - "move_mode": "walk", - "action": "" + "move_mode": "fly", + "action": "stop_flying" }, { "x": 3725.26,