455 lines
17 KiB
JavaScript
455 lines
17 KiB
JavaScript
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 jsonFiles = filterFilesInTaskDir(pathTask.label);
|
||
const recordFile = getRecordFilePath(account, pathTask);
|
||
const lines = 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 = filterFilesInTaskDir(pathTask.label);
|
||
|
||
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); // 去除⬇️指示
|
||
selectedMaterials.push(entry);
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|