Files

460 lines
17 KiB
JavaScript
Raw Permalink 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.

eval(file.readTextSync("lib/lib.js"));
const settingFile = "settings.json";
const defaultTime = getDefaultTime();
const CooldownDataBase = readRefreshInfo("CooldownData.txt");
let stopAtTime = null;
let currentParty = null;
let runMode = settings.runMode;
let subscribeMode = settings.subscribeMode;
class ReachStopTime extends Error {
constructor(message) {
super(message);
this.name = "ReachStopTime";
}
}
class UserCancelled extends Error {
constructor(message) {
super(message);
this.name = "UserCancelled";
}
}
(async function () {
if (!file.IsFolder("pathing")) {
let batFile = "SymLink.bat";
const folderPath = getScriptDirPath();
if (folderPath) {
batFile = `${folderPath}\\${batFile}`;
}
log.error("{0}文件夹不存在,请双击运行下列位置的脚本以创建文件夹链接\n{1}", "pathing", batFile);
return;
}
if (!runMode) {
const defaultRunMode = "采集选中的材料";
log.warn("运行模式 未选择或无效: {0},默认为{1}", runMode, defaultRunMode);
runMode = defaultRunMode;
}
if (!subscribeMode) {
const defaultSubscribeMode = "每次自动扫描,并采集扫描到的所有材料";
log.warn("已订阅的任务目录的处理方式 未选择或无效: {0},默认为{1}", subscribeMode, defaultSubscribeMode);
subscribeMode = defaultSubscribeMode;
}
if (!settings.runMode || !settings.subscribeMode) {
await sleep(3000);
}
log.info("当前运行模式:{0}", runMode);
if (runMode === "扫描文件夹更新可选材料列表") {
await runScanMode();
} else if (runMode === "采集选中的材料") {
let startTime = logFakeScriptStart();
log.info("已订阅的任务目录的处理方式:{0}", subscribeMode);
if (subscribeMode === "每次自动扫描,并采集扫描到的所有材料") {
await runScanMode();
}
await runGatherMode();
logFakeScriptEnd({ startTime: startTime });
} else if (runMode === "清除运行记录(重置材料刷新时间)") {
await runClearMode();
}
})();
// 扫描文件夹更新可选材料列表
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);
// 将地方特产按照国家顺序排序
const countryList = ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "至冬"];
const sortedList = pathList.slice().sort((a, b) => {
const getRegion = (p) => p.split("\\")[2];
const aIndex = countryList.indexOf(getRegion(a));
const bIndex = countryList.indexOf(getRegion(b));
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
});
// 3. 处理每个路径
let count = 0;
actions_map = {};
for (const path of sortedList) {
const info = getCooldownInfoFromPath(path);
const jsonFiles = filterFilesInTaskDir(info.label);
if (jsonFiles.length === 0) {
log.info("{0}内无json文件跳过", path);
} else if (info.coolType === null) {
log.warn("路径{0}未匹配到对应的刷新机制,跳过", path);
} else {
config.push({
name: info.name,
label: "⬇️ " + info.label,
type: "checkbox",
});
count += 1;
const actions = scanSpecialCollectMethod(jsonFiles);
if (actions.length > 0) {
actions_map[path] = actions;
}
}
}
// 4. 写入新的配置(格式化输出)
file.writeTextSync(settingFile, JSON.stringify(config, null, 2));
log.info("共{0}组有效路线,请在脚本配置中勾选需要采集的材料", count);
// 5. 分析所需角色信息
analysisCharacterRequirement(actions_map);
await sleep(3000);
}
// 采集选中的材料
async function runGatherMode() {
if (settings.stopAtTime) {
stopAtTime = settings.stopAtTime;
log.info("脚本已被配置为达到{0}后停止运行", stopAtTime);
}
const selectedMaterials = getSelectedMaterials();
if (selectedMaterials.length === 0) {
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
return;
}
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 getGameAccount(settings.iHaveMultipleAccounts);
log.info("为{0}采集材料并管理CD", account);
await switchPartySafely(settings.partyName);
currentParty = settings.partyName;
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},终止运行", stopAtTime);
} else if (e instanceof UserCancelled) {
log.info("用户取消,终止运行");
} else {
throw e;
}
}
}
// 清除运行记录(重置材料刷新时间)
async function runClearMode() {
const selectedMaterials = getSelectedMaterials();
if (selectedMaterials.length === 0) {
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
}
const resetTimeStr = formatDateTime(getDefaultTime());
let account = await getGameAccount(settings.iHaveMultipleAccounts);
for (const pathTask of selectedMaterials) {
const recordFile = getRecordFilePath(account, pathTask);
const lines = pathTask.jsonFiles.map((filePath) => {
return `${basename(filePath)}\t${resetTimeStr}`;
});
const content = lines.join("\n");
file.writeTextSync(recordFile, content);
log.info("已重置{0}的刷新时间", pathTask.label);
}
log.info("已重置{0}组刷新时间。如需重置所有材料刷新时间请直接删除record目录下对应账号的文件夹", selectedMaterials.length);
}
function scanSpecialCollectMethod(jsonFiles) {
const actions = jsonFiles.flatMap((filePath) => {
const data = JSON.parse(file.readTextSync(filePath));
return data.positions.map((p) => p.action).filter((a) => a);
});
return [...new Set(actions)];
}
function readRefreshInfo(filePath) {
const lines = file.readTextSync(filePath).split(/\r?\n/);
const dict = {};
for (const line of lines) {
if (!line.trim()) continue; // 跳过空行
const [key, value] = line.split(":");
dict[key.trim()] = value ? value.trim() : "";
}
return dict;
}
function analysisCharacterRequirement(actions_map) {
const result = {};
for (const [key, values] of Object.entries(actions_map)) {
const newKey = key.replace(/^pathing\\/, "");
for (const v of values) {
if (!result[v]) {
result[v] = [];
}
result[v].push(newKey);
}
}
let collect_methods = {};
for (const [key, value] of Object.entries(result)) {
if (key.endsWith("_collect") || key === "fight" || key === "combat_script") {
collect_methods[key] = value;
}
}
collect_methods = Object.fromEntries(Object.entries(collect_methods).sort((a, b) => b[1].length - a[1].length));
log.info(
"角色需求: {1}条路线需要纳西妲,{2}条路线需要水元素,{3}条路线需要雷元素,{4}条路线需要风元素,{5}条路线需要火元素;{6}条路线需要执行自动战斗;{7}条路线使用了战斗策略脚本(含挖矿等非战斗用途)",
collect_methods["nahida_collect"]?.length || 0,
collect_methods["hydro_collect"]?.length || 0,
collect_methods["electro_collect"]?.length || 0,
collect_methods["anemo_collect"]?.length || 0,
collect_methods["pyro_collect"]?.length || 0,
collect_methods["fight"]?.length || 0,
collect_methods["combat_script"]?.length || 0
);
const nameMap = {
nahida_collect: "纳西妲",
hydro_collect: "水元素",
electro_collect: "雷元素",
anemo_collect: "风元素",
pyro_collect: "火元素",
fight: "自动战斗",
combat_script: "战斗策略脚本",
};
let analysisResult = {};
for (const [key, value] of Object.entries(collect_methods)) {
const name = nameMap[key] || key;
analysisResult[name] = value;
}
const outFile = `${getScriptDirPath()}\\各条路线所需角色.txt`;
let text = "";
// text = JSON.stringify(analysisResult, null, 2);
for (const [key, values] of Object.entries(analysisResult)) {
text += `${key}\n`;
for (const v of values) {
text += ` ${v}\n`;
}
}
file.writeTextSync(outFile, text);
log.info("详细路线需求见{x},可考虑组两支队伍{0}和{1}以满足采集需要", outFile, "钟纳水雷", "钟纳火风");
}
function getRecordFilePath(account, pathTask) {
const taskName = pathTask.name.replace(/^OPT_/, "");
return `record/${account}/${taskName}.txt`;
}
function filterFilesInTaskDir(taskDir) {
return getFilesByExtension("pathing\\" + taskDir, ".json");
}
async function runPathScriptFile(jsonPath) {
await pathingScript.runFile(jsonPath);
//捕获任务取消的信息并跳出循环
try {
await sleep(10);
} catch (error) {
return error.toString();
}
return false;
}
async function runPathTaskIfCooldownExpired(account, pathTask) {
const recordFile = getRecordFilePath(account, pathTask);
const jsonFiles = pathTask.jsonFiles;
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 pathName = fileName.split(".")[0];
const lastTime = recordMap[fileName] || defaultTime;
const progress = `[${i + 1}/${jsonFiles.length}]`;
if (stopAtTime && isTargetTimeReached(stopAtTime)) {
throw new ReachStopTime("达到设置的停止时间,终止运行");
}
if (Date.now() > lastTime) {
let pathStart = logFakePathStart(fileName);
log.info(`${progress}{0}: 开始执行`, pathName);
let pathStartTime = new Date();
// 延迟抛出`UserCancelled`,以便正确更新运行记录
const cancel = await runPathScriptFile(jsonPath);
let diffTime = new Date() - pathStartTime;
if (diffTime < 1000) {
// "队伍中没有对应元素角色"的错误不会抛出为异常,只能通过路径文件迅速结束来推测
if (settings.partyName && settings.partyName2nd) {
let newParty = (currentParty === settings.partyName) ? settings.partyName2nd : settings.partyName;
log.info("当前队伍{0}缺少该路线所需角色,尝试切换到{1}", currentParty, newParty);
await switchPartySafely(newParty);
await runPathScriptFile(jsonPath);
}
} else if (diffTime > 5000) {
recordMap[fileName] = calculateNextRunTime(new Date(), jsonPath);
const lines = [];
for (const [p, t] of Object.entries(recordMap)) {
lines.push(`${p}\t${formatDateTime(t)}`);
}
const content = lines.join("\n");
file.writeTextSync(recordFile, content);
log.info(`${progress}{0}: 已完成,下次刷新: ${formatDateTimeShort(recordMap[fileName])}`, pathName);
} else {
log.info(`${progress}{0}: 执行时间过短,不更新记录`, pathName);
}
logFakePathEnd(fileName, pathStart);
if (cancel) {
throw new UserCancelled(cancel);
}
} else {
log.info(`${progress}{0}: 已跳过 (${formatDateTimeShort(recordMap[fileName])}刷新)`, pathName);
}
}
}
/**
* 根据路径逐级查找最匹配的物品,返回去除前缀的路径、标准化名称、刷新时间
* @param {string} fullPath - 单个完整路径(包含公共前缀)
* @returns {{ label: string, name: string, coolType: string }}
*/
function getCooldownInfoFromPath(fullPath) {
const parts = fullPath.split(/[\\/]/); // 支持 \ 或 / 分隔符
let cooldown = null;
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 = calculateNextRefreshTime(base, coolType);
return nextTime;
}
function getSelectedMaterials() {
const configText = file.readTextSync(settingFile);
const config = JSON.parse(configText); // 配置数组
const selectedMaterials = [];
const selectAllMaterials = subscribeMode.includes("采集扫描到的所有材料");
for (const entry of config) {
if (entry.name && entry.name.startsWith("OPT_") && entry.type === "checkbox") {
if (selectAllMaterials || settings[entry.name] === true) {
let index = entry.label.indexOf(" ");
entry.label = entry.label.slice(index + 1); // 去除⬇️指示
const jsonFiles = filterFilesInTaskDir(entry.label);
if (jsonFiles.length > 0) {
entry.jsonFiles = jsonFiles;
selectedMaterials.push(entry);
} else {
log.debug("跳过空文件夹: {0}", entry.label);
}
}
}
}
const materialDict = {};
selectedMaterials.forEach((item) => {
const label = item.label;
const match = label.match(/\\(.*?)\\\1/); // \落落莓\落落莓@Author
let materialName;
if (match) {
materialName = match[1];
} else {
const parts = label.split("\\");
materialName = parts[parts.length - 1];
}
if (!materialDict[materialName]) {
materialDict[materialName] = [];
}
materialDict[materialName].push(item);
});
const firstRoutes = [];
const multiRoutes = {};
for (const materialName in materialDict) {
const routes = materialDict[materialName];
if (routes.length > 0) {
firstRoutes.push(routes[0]);
if (materialDict[materialName].length > 1) {
multiRoutes[materialName] = materialDict[materialName];
}
}
}
const countOfMultiRoutes = Object.keys(multiRoutes).length;
if (countOfMultiRoutes > 0) {
let text = `${countOfMultiRoutes}种材料存在多个版本的路线:\n`;
for (const [key, values] of Object.entries(multiRoutes)) {
text += `${key}\n`;
for (const v of values) {
text += ` ${v.label}\n`;
}
}
log.debug(text);
if (settings.acceptMultiplePathOfSameMaterial) {
log.warn("{0}种材料选中了多个版本的路线(详见日志文件),根据脚本设置,将执行全部版本", countOfMultiRoutes);
} else {
log.warn("{0}种材料选中了多个版本的路线(详见日志文件),默认只执行第一个版本", countOfMultiRoutes);
return firstRoutes;
}
}
return selectedMaterials;
}