js: 带CD管理和目标设定的自动采集,全新2.0版本 (#2556)

* js: 带CD管理和目标设定的自动采集,全新2.0版本

* Remove this-Fish from manifest acknowledgments per request

* Fix issues identified by bot review
This commit is contained in:
Patrick-Ze
2025-12-25 10:07:28 +08:00
committed by GitHub
parent 62e26e9f31
commit 5a52623848
16 changed files with 2596 additions and 1291 deletions

View File

@@ -1,69 +0,0 @@
沉玉仙茗: 24小时
发光髓: 12小时
蝴蝶翅膀: 12小时
晶核: 12小时
鳗肉: 12小时
螃蟹: 12小时
禽肉: 12小时
青蛙: 12小时
鳅鳅宝玉: 12小时
神秘的肉: 12小时
兽肉: 12小时
「冷鲜肉」: 12小时
蜥蜴尾巴: 12小时
鱼肉: 12小时
白萝卜: 每天0点
薄荷: 每天0点
澄晶实: 每天0点
墩墩桃: 每天0点
海草: 每天0点
红果果菇: 每天0点
胡萝卜: 每天0点
金鱼草: 每天0点
堇瓜: 每天0点
烬芯花: 每天0点
久雨莲: 每天0点
颗粒果: 每天0点
苦种: 每天0点
莲蓬: 每天0点
烈焰花花蕊: 每天0点
马尾: 每天0点
蘑菇: 每天0点
茉洁草: 每天0点
鸟蛋: 每天0点
泡泡桔: 每天0点
苹果: 每天0点
日落果: 每天0点
树莓: 每天0点
松果: 每天0点
松茸: 每天0点
甜甜花: 每天0点
汐藻: 每天0点
香辛果: 每天0点
星蕈: 每天0点
须弥蔷薇: 每天0点
枣椰: 每天0点
竹笋: 每天0点
烛伞蘑菇: 每天0点
宿影花: 每天0点
寒涌石: 每天0点
白灵果: 每天0点
夏槲果: 每天0点
沉玉仙茗: 24小时
晶蝶: 每天4点
铁块: 每天0点
白铁块: 每2天0点
电气水晶: 每2天0点
星银矿石: 每2天0点
萃凝晶: 每3天0点
水晶块: 每3天0点
紫晶块: 每3天0点
虹滴晶: 每3天0点
奇异的「牙齿」: 46小时
冰雾花花朵: 46小时
冰雾花: 46小时
烈焰花花蕊: 46小时
烈焰花: 46小时
地方特产: 46小时

View File

@@ -1,8 +1,21 @@
**由于使用了尚处于测试版BetterGI中的API使用稳定版BetterGI的用户请等待`0.54.1`或更高的版本发布后再订阅此脚本**
# 功能特点
- 自动同步你订阅的地图追踪任务,无需手动复制文件夹
- 自动匹配各类材料的CD支持联机模式会识别是在谁的世界进行采集
- 可识别背包物品数量,并按设定的目标数量按需运行
- 方便的材料选择界面,并支持按地区、按类别批量选择
- 可设置一个或多个不运行的时间段
- 采集过程自动切换合适的队伍
# 使用前准备
在脚本仓库页面阅读此文档会比在BGI的已订阅脚本界面获得更好的渲染效果
**双击运行脚本所在目录下的`SymLink.bat`文件,以创建符号链接。**
(脚本所在目录为你的BetterGI安装位置下面的`repo\js\CD-Aware-AutoGather`文件夹)
(在BGI的`全自动——JS脚本`界面,右键点击本脚本,选择`打开目录`可快速打开脚本所在目录)
此操作只需要一次。运行后,脚本下的`pathing`文件夹将指向Better GI的地图追踪文件夹`User\AutoPathing`你通过Better GI新增或删除地图追踪任务后脚本这边看到的也是修改后的。
@@ -12,46 +25,152 @@
## 1. 扫描文件夹更新可选材料列表
扫描脚本`pathing`目录等价于你在Better GI里订阅的地图追踪任务目录下的`地方特产``矿物``食材与炼金`内的材料,并自动匹配材料的刷新时间
扫描脚本`pathing`目录等价于你在Better GI里订阅的地图追踪任务目录下的`地方特产``矿物``食材与炼金`内的材料,并更新脚本可用的配置菜单
扫描完成后,将自动更新脚本可用的配置菜单。此时再次打开右键的`修改JS脚本自定义配置`,将看到新增了多个配置项,其中包含刚刚扫描到的材料目录
运行后,再次打开右键的`修改JS脚本自定义配置`,将看到新增了多个配置项。
![preview.png](https://foruda.gitee.com/images/1749967868807757262/ada1abf2_9716310.png)
如果你订阅了很多地图追踪任务,那么扫描结果也会比较多,选项列表也会比较长,但不影响脚本运行。
只有你新增或者删除了地图追踪任务的订阅时,才需要运行此模式。
完成扫描后会自动设置下次的脚本运行模式为`采集选中的材料`
## 2. 采集选中的材料
此模式下有这些选项可以配置:
| 选项 | 说明 |
| ---- | ---- |
| `地图追踪`中已订阅的任务目录的处理方式 | 有三种处理方式:<br>- `每次自动扫描,并采集扫描到的所有材料`<br>- `手动扫描,并采集扫描到的所有材料`<br>- `手动扫描,只采集已勾选的材料`<br>`自动扫描`会在每次执行采集前扫描当前已订阅的任务目录,`手动扫描`则是手动将运行模式切换到扫描模式执行脚本进行扫描。<br>`采集所有`会无视后面的每个材料⬇️是否选中,`采集已勾选`则是根据勾选的列表进行采集 |
| 设置首选队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 |
| 设置备选队伍名称 | 首选队伍缺少对应的采集角色时使用。<br>两支队伍的名称不要存在包含关系,例如不能一支叫`特产`一支叫`特产备选` |
| 停止运行时间 | 超过此时间后停止后续的任务会等待正在运行的那条json路线结束。 |
| 我肝的账号不止一个 | 如果你有多个账号可在输入框内为每个游戏帐号填写一个唯一的字符串以使他们的刷新时间分开保存互不干扰。这个唯一的字符串并不必须是帐号的UID只要互不相同就可以了。 <br>如果填空会自动使用原神中右下角UID作为区分账号的唯一字符串 |
| 即使同一种材料有多个版本的路线,也全都执行采集 | 如果某种材料选中了多个版本的路线(常见于不同作者),默认只会执行第一个。勾选此选项后会每个版本都执行,可能造成部分点位重复(空跑)。 |
| `↓` 地方特产\稻妻\绯樱绣球 | 根据你订阅的路径追踪任务数量,这里将会显示相应个数的选择框。<br>勾选后将执行你选中的条目的采集任务。<br>Tip: `↓`符号是在提示你应该勾选文本下面的选择框 |
**推荐的配置**: 运行模式设置为`采集选中的材料`,已订阅的任务目录的处理方式设置为`每次自动扫描,并采集扫描到的所有材料`。这样一来,待采集的材料列表自动保持与你订阅的列表一致,新增/删除材料也直接通过BetterGI的界面执行每次直接运行脚本即可。
运行此模式后将按照你勾选的条目执行相应的采集任务。每执行完一条json路线后将会计算它的下次刷新时间并写入`record`文件夹下的记录文件。下次运行脚本时,未刷新的路线将自动跳过。
采集过程中,如果所有材料均达到了你设置的目标数量,或者到达你设置的不执行时间段,会自动停止运行。
可以同时勾选多种材料,会逐个进行采集。
此模式下有这些选项可以配置:
### 基本配置
| 选项 | 说明 |
| ---- | ---- |
| 按关键词筛选材料路线 | 默认为黑名单模式。此时,文件路径中含有任意一个关键词的路线,在扫描时会被忽略。其他模式见后文 |
| 设置首选队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 |
| 设置备选队伍名称 | 首选队伍缺少对应的采集角色时使用。<br>两支队伍的名称不要存在包含关系,例如不能一支叫`特产`一支叫`特产备选` |
| 不在以下时间段内运行 | 字面意思。如果脚本启动时间在设定的时间段内,会立即终止;如果运行过程中进入设定的时间段内,也会停止后续路线。支持设置多个时间段,用空格分隔即可,比如`05:20-09:28 21:28-00:20` |
| 物品采集数量目标 | 填写目标数量,留空表示不限制。也可以设置为`csv`将按csv文件的配置详细设定不同材料的目标具体见后文 |
| 使用指定的账号名保存运行记录 | 留空时将自动识别UID将其作为账号名记录材料刷新时间。你也可以主动填写账号名称不推荐会导致联机互采时例如大小号无法正确维护材料刷新时间建议仅在你遇到脚本无法稳定识别你的UID时填写 |
### 材料采集配置
这部分配置分为4个大类根据你订阅的路线可能只会出现其中的部分配置。
为了方便用户查找:
- “地区”类型的选项,是按照在游戏中开放的顺序排序的。
- 每个小类别下的选项,是按照拼音的先后顺序排序的。
**【常用采集选项】**
开发了按类别、按地区批量选择的功能,并和常用材料汇集到了一起以方便使用,如图所示。
`按大类选择``按地区选择地方特产`都是批量选择。批量选择和后面的具体项选择可以结合使用,实现灵活的采集目标设定。
比如勾选 `按大类选择`的矿物 + `按地区选择地方特产`里的蒙德 + `单独选择地方特产`下的稻妻地区的绯樱绣球。
![pic-1](assets/pic-1.png)
**【单独选择地方特产】**
![pic-2](assets/pic-2.png)
**【食材与炼金】**
![pic-3](assets/pic-3.png)
**【对于具有多版本路线的物品,选择要使用的路线】**
如果你为某种材料订阅了多个版本的路线,可以在这里进行选择其中的一个或多个选项。
如果你没有指定路线,会默认执行第一项的路线。
![pic-4](assets/pic-4.png)
### 队伍配置推荐
采集任务可能用到的元素共有`火水雷风`4种此外还有挖矿类如钟离以及纳西妲两个类型可以考虑建立两支队伍`钟纳水雷``钟纳火风`,即可满足所有采集任务的需要。
支持使用配置组`更多功能`——`日志分析`分析运行记录(参考了[mno](https://github.com/Bedrockx)大佬的写法)
配置好两支队伍后,脚本会在执行路线前计算所需角色,如果当前队伍不满足要求,会自动切换到另一支队伍
![log_analysis.png](https://foruda.gitee.com/images/1749967993135535153/3bbeecd3_9716310.png)
注意:队伍切换功能在联机模式下无法执行,因此联机采集时请先调整好队伍中的角色。
## 3. 清除运行记录(重置材料刷新时间)
# 进阶用法
此模式下,相关的选项只有`我肝的账号不止一个`和以`↓`开头的任务名,作用同上文。
### 1. 不想每次订阅材料路线后都要在脚本设置中再勾选,怎么办?
运行此模式后,将重置你选中的任务相应材料的刷新时间
利用好`按大类选择`选项。你可以勾选上全部三个大类或者你实际需要的类别然后只要在BGI本体里订阅路线即可
如果你需要删除全部运行记录,可以直接删除脚本`record`文件夹下,以账号为名称的文件夹内的文件。
### 2. 怎么清除运行记录(重置材料刷新时间)?
运行记录保存在脚本所在路径的`record`文件夹中以UID或者用户指定的名字作为账号文件夹。打开子文件夹后删除对应材料的txt文件即可。
### 3. `按关键词筛选材料路线`的进阶用法
如前面配置描述中所述,此选项默认以黑名单模式运行,设置的关键词即为黑名单,在扫描时会被忽略。可以在选项的开头加上`include:`,将切换为以白名单模式运行,例如`include:无草神`将只扫描含有"无草神"字样的路线。
或者更进阶的,使用`regex:`开头,将完全使用你设置的正则表达式进行筛选。如果你不了解什么是正则表达式,说明你大概率也不需要这种方式。
使用该选项设置的进阶用法时,希望你充分理解了自己的设置会产生什么样的效果。
### 4. 在`物品采集数量目标`选项中,如何为不同物品设置不同的目标?
请将该选项设置为`csv`然后执行一次材料扫描模式。扫描完成后在record下找到对应账号的子文件夹。打开这个子文件夹会找到一个`采集目标.csv`的csv文件。
你可以用Excel或者WPS等打开此csv文件打开后就是一个表格如果你了解csv文件格式也可以用任意文本编辑器打开不过不推荐。表格内容示例如下
| 物品 | 目标数量 |
| -------------- | ---- |
| 地方特产 | |
| 矿物 | |
| 食材与炼金 | |
| 地方特产\\璃月 | |
| 地方特产\\稻妻 | |
| 矿物\\萃凝晶 | |
| 矿物\\虹滴晶 | |
| 食材与炼金\\晶蝶 | |
| 食材与炼金\\松果 | |
| 地方特产\\璃月\\夜泊石 | |
| 地方特产\\璃月\\星螺 | |
| 地方特产\\稻妻\\绯樱绣球 | |
| 地方特产\\须弥\\树王圣体菇 | |
默认情况下`目标数量`这一列是空的填写具体的数字保存文件后关闭Excel或者其他你用来编辑csv的工具。然后再次运行脚本即可。
**如你所见这个csv表格也具有和脚本选项中相似的层次关系。 是的,数量是按层次继承的~** 🎉
- 如果低层次的材料没有设置目标,则会自动尝试较高层次所设定的数量
- 如果向上遍历所有层次都没有目标数量,则表示不限制数量
- 数量为`0`将不进行采集
利用好脚本设置中的批量选择功能和这个层次关系,可以轻松实现灵活的采集目标。比如:
- 希望背包材料能立即把一个角色拉到90级于是在`按大类选择`中勾选了地方特产并将地方特产类别的数量设置为168
- 但是,又觉得不会再用到树王圣体菇,所以把`地方特产\须弥\树王圣体菇`的数量设置为0
- 璃月的特产希望多采集一点,那就把`地方特产\璃月`的数量增加了一点到199
- 还想做用`红炉一点雪`填满背包,那就把对应的特产`地方特产\稻妻\绯樱绣球`设置为9999
- 想要随时能锻造武器,于是于是在`按大类选择`中勾选了矿物并将矿物类别的数量设置为50
- 不想抓晶蝶随便设个100个晶蝶吧
- 对了还有每周参量质变仪要用的薄荷要不就2倍数量吧300
- ……
选好材料和数量,配置好队伍,然后把脚本加到你的每日配置组或一条龙中,从此实现背包自由。
# 致谢
此脚本的2.0版本进行了重写基于新的API重新设计了主要逻辑以解决此前我想要处理却不能的痛点。在此
- 诚挚致敬[鸭蛋老师](https://github.com/huiyadanli)和BGI的诸位贡献者是各位使我的提瓦特之旅得以苟延残喘
- 得益于[FishmanTheMurloc](https://github.com/FishmanTheMurloc)的鼎力相助,脚本才能实现无图的批量材料扫描
- 受惠于[JamisHoo](https://github.com/JamisHoo)的配置界面优化,使得更便捷的材料选择成为可能
- 感谢[this-Fish](https://github.com/this-Fish)的改进基于坐标判断是否更新记录、将材料是否刷新的检查提前都是沿袭的TA的思路
最后,要特别感谢绫华,是她陪伴了我的提瓦特之旅。在弃坑之后,唯有这份牵挂,支撑着我重新回到这里。
没有她就没有今天的这个脚本。
![in-a-league-of-her-own](https://ayaka20.ggff.net/img/ayaka.png)

View File

@@ -0,0 +1,123 @@
{
"冰": [
"神里绫华",
"丝柯克",
"爱可菲",
"茜特菈莉",
"夏洛蒂",
"莱欧斯利",
"菲米尼",
"米卡",
"莱依拉",
"申鹤",
"埃洛伊",
"优菈",
"罗莎莉亚",
"甘雨",
"迪奥娜",
"七七",
"凯亚",
"重云"
],
"火": [
"杜林",
"玛薇卡",
"阿蕾奇诺",
"嘉明",
"夏沃蕾",
"林尼",
"迪希雅",
"托马",
"宵宫",
"烟绯",
"胡桃",
"辛焱",
"可莉",
"迪卢克",
"安柏",
"班尼特",
"香菱"
],
"水": [
"爱诺",
"塔利雅",
"玛拉妮",
"希格雯",
"芙宁娜",
"那维莱特",
"妮露",
"坎蒂丝",
"夜兰",
"神里绫人",
"珊瑚宫心海",
"达达利亚",
"莫娜",
"芭芭拉",
"行秋"
],
"风": [
"雅珂达",
"伊法",
"梦见月瑞希",
"蓝砚",
"恰斯卡",
"闲云",
"琳妮特",
"流浪者",
"珐露珊",
"鹿野院平藏",
"早柚",
"枫原万叶",
"魈",
"温迪",
"琴",
"砂糖"
],
"雷": [
"菲林斯",
"伊涅芙",
"瓦雷莎",
"伊安珊",
"欧洛伦",
"克洛琳德",
"赛索斯",
"赛诺",
"多莉",
"久岐忍",
"八重神子",
"雷电将军",
"九条裟罗",
"刻晴",
"菲谢尔",
"丽莎",
"北斗",
"雷泽"
],
"草": [
"奈芙尔",
"菈乌玛",
"基尼奇",
"艾梅莉埃",
"绮良良",
"白术",
"卡维",
"艾尔海森",
"瑶瑶",
"纳西妲",
"提纳里",
"柯莱"
],
"岩": [
"希诺宁",
"卡齐娜",
"千织",
"娜维娅",
"云堇",
"荒泷一斗",
"五郎",
"阿贝多",
"钟离",
"凝光",
"诺艾尔"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,95 @@
[
{
"name": "runMode",
"type": "select",
"label": "运行模式",
"options": [
"扫描文件夹更新可选材料列表",
"采集选中的材料"
],
"default": "扫描文件夹更新可选材料列表"
},
{
"name": "filterPathByKeywords",
"type": "input-text",
"label": "按关键词筛选材料路线(使用空格分隔,默认为黑名单模式)",
"default": "低成功率 不建议"
},
{
"name": "partyName",
"type": "input-text",
"label": "设置首选队伍名称(留空则不进行切换)"
},
{
"name": "partyName2nd",
"type": "input-text",
"label": "设置备选队伍名称(首选队伍缺少对应的采集角色时使用)"
},
{
"name": "excludeTimeRange",
"type": "input-text",
"label": "不在以下时间段内运行HH:mm-HH:mm24小时制"
},
{
"name": "targetCountOfSelected",
"type": "input-text",
"label": "物品采集数量目标可使用csv为不同物品设置不同数量"
},
{
"name": "manualSetAccountName",
"type": "input-text",
"label": "使用指定的账号名保存运行记录"
},
{
"type": "separator"
},
{
"label": "\n【常用采集选项】\n\n按大类选择",
"type": "multi-checkbox",
"name": "selectByCategory",
"options": [
"地方特产",
"矿物",
"食材与炼金"
]
},
{
"label": "按地区选择地方特产",
"type": "multi-checkbox",
"name": "selectLocalSpecialtyByCountry",
"options": [
"蒙德",
"璃月",
"稻妻",
"须弥",
"枫丹",
"纳塔",
"挪德卡莱"
]
},
{
"label": "矿物",
"type": "multi-checkbox",
"name": "selectForgingOre",
"options": [
"白铁块",
"水晶块",
"星银矿石",
"紫晶块",
"萃凝晶",
"虹滴晶"
]
},
{
"label": "特殊物品",
"type": "multi-checkbox",
"name": "selectMiscellaneous",
"options": [
"晶蝶",
"沉玉仙茗"
]
},
{
"type": "separator"
}
]

View File

@@ -0,0 +1,246 @@
/**
* @author Ayaka-Main
* @link https://github.com/Patrick-Ze
* @description 对背包API的易用包装。使用方法: 将此文件放在脚本目录下的 lib 文件夹中,然后在你的脚本开头处执行下面这行:
eval(file.readTextSync("lib/inventory.js"));
*/
let scriptContext = {
version: "1.0",
};
// 原本是csv格式但是为了方便js重用还是内置在代码中
const csvText = `物品,刷新机制,背包分类
「冷鲜肉」,12小时,材料
发光髓,12小时,材料
蝴蝶翅膀,12小时,材料
晶蝶,12小时,材料
晶核,12小时,材料
鳗肉,12小时,材料
螃蟹,12小时,材料
禽肉,12小时,材料
青蛙,12小时,材料
鳅鳅宝玉,12小时,材料
神秘的肉,12小时,材料
兽肉,12小时,材料
蜥蜴尾巴,12小时,材料
鱼肉,12小时,材料
沉玉仙茗,24小时,材料
便携轴承,46小时,材料
冰雾花花朵,46小时,材料
苍晶螺,46小时,材料
赤念果,46小时,材料
初露之源,46小时,材料
悼灵花,46小时,材料
嘟嘟莲,46小时,材料
绯樱绣球,46小时,材料
风车菊,46小时,材料
钩钩果,46小时,材料
鬼兜虫,46小时,材料
海灵芝,46小时,材料
海露花,46小时,材料
虹彩蔷薇,46小时,材料
湖光铃兰,46小时,材料
劫波莲,46小时,材料
晶化骨髓,46小时,材料
绝云椒椒,46小时,材料
枯叶紫英,46小时,材料
浪沫羽鳃,46小时,材料
烈焰花花蕊,46小时,材料
琉璃百合,46小时,材料
琉璃袋,46小时,材料
琉鳞石,46小时,材料
落落莓,46小时,材料
鸣草,46小时,材料
慕风蘑菇,46小时,材料
霓裳花,46小时,材料
帕蒂沙兰,46小时,材料
蒲公英籽,46小时,材料
奇异的「牙齿」,46小时,材料
青蜜莓,46小时,材料
清水玉,46小时,材料
清心,46小时,材料
柔灯铃,46小时,材料
肉龙掌,46小时,材料
塞西莉亚花,46小时,材料
沙脂蛹,46小时,材料
珊瑚真珠,46小时,材料
圣金虫,46小时,材料
石珀,46小时,材料
树王圣体菇,46小时,材料
霜盏花,46小时,材料
天云草实,46小时,材料
万相石,46小时,材料
微光角菌,46小时,材料
小灯草,46小时,材料
星螺,46小时,材料
血斛,46小时,材料
夜泊石,46小时,材料
幽灯蕈,46小时,材料
幽光星星,46小时,材料
月莲,46小时,材料
月落银,46小时,材料
云岩裂叶,46小时,材料
灼灼彩菊,46小时,材料
子探测单元,46小时,材料
白铁块,每2天0点,材料
电气水晶,每2天0点,材料
星银矿石,每2天0点,材料
萃凝晶,每3天0点,材料
虹滴晶,每3天0点,材料
水晶块,每3天0点,材料
紫晶块,每3天0点,材料
白灵果,每天0点,材料
白萝卜,每天0点,材料
薄荷,每天0点,材料
澄晶实,每天0点,材料
墩墩桃,每天0点,材料
海草,每天0点,材料
寒涌石,每天0点,材料
红果果菇,每天0点,材料
胡萝卜,每天0点,材料
金鱼草,每天0点,材料
堇瓜,每天0点,材料
烬芯花,每天0点,材料
久雨莲,每天0点,材料
颗粒果,每天0点,材料
苦种,每天0点,材料
莲蓬,每天0点,材料
马尾,每天0点,材料
蘑菇,每天0点,材料
茉洁草,每天0点,材料
鸟蛋,每天0点,材料
树莓,每天0点,材料
松果,每天0点,材料
松茸,每天0点,材料
宿影花,每天0点,材料
甜甜花,每天0点,材料
铁块,每天0点,材料
汐藻,每天0点,材料
夏槲果,每天0点,材料
香辛果,每天0点,材料
须弥蔷薇,每天0点,材料
枣椰,每天0点,材料
竹笋,每天0点,材料
泡泡桔,每天0点,食物
苹果,每天0点,食物
日落果,每天0点,食物
星蕈,每天0点,食物
烛伞蘑菇,每天0点,食物
`
const materialMetadata = {};
function parseCsvTextToDict() {
// 1. 将文本按行分割成数组
const lines = csvText.trim().split("\n");
// 预期标题是:['物品', '刷新机制', '背包分类']
const headers = lines[0].split(",").map((header) => header.trim());
// 检查标题是否符合预期,防止解析错误
if (headers[0] !== "物品" || headers.length < 3) {
console.error("CSV格式的标题行不符合预期。");
return {};
}
// 确定我们需要的值对应的索引
const cdIndex = headers.indexOf("刷新机制");
const typeIndex = headers.indexOf("背包分类");
// 3. 处理数据行,构建结果字典
const resultDict = {};
// 从第二行(索引 1开始遍历数据
const typeMap = {"食物": "Food", "材料": "Materials"};
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (line === "") continue; // 跳过空行
// 将数据行按逗号分割
const values = line.split(",").map((value) => value.trim());
// 确保数据行有足够的值
if (values.length > Math.max(cdIndex, typeIndex) && values[0]) {
const itemName = values[0];
const cdValue = values[cdIndex];
const typeValue = typeMap[values[typeIndex]];
// 存入结果字典,以物品名为 key
resultDict[itemName] = {
cd: cdValue,
type: typeValue,
};
}
}
return resultDict;
}
/**
* 获取背包中物品的数量。
* 如果没有找到,则为-1如果找到了但数字识别失败则为-2
*
* 暂不支持 冷鲜肉, 红果果菇, 奇异的「牙齿」
*/
async function getItemCount(itemList=null, retry=true) {
if (Object.keys(materialMetadata).length === 0) {
Object.assign(materialMetadata, parseCsvTextToDict());
}
if (typeof itemList === "string") {
itemList = [itemList];
} else if (itemList == null || itemList.length === 0) {
itemList = Object.keys(materialMetadata);
}
const renameMap = {"晶蝶": "晶核", "「冷鲜肉」":"冷鲜肉"};
const groupByType = {};
for (const itemName of itemList) {
const metadata = materialMetadata[itemName];
const itemType = metadata?.type ?? "Materials";
if (!metadata?.type) {
log.warn("未查找到{0}所属的背包分类,默认它是{1}", itemName, itemType);
}
const normalizedName = renameMap[itemName] || itemName;
groupByType[itemType] = groupByType[itemType] || [];
groupByType[itemType].push(normalizedName);
}
let results = {};
for (const type in groupByType) {
const names = groupByType[type];
const countResult = await dispatcher.runTask(new SoloTask("CountInventoryItem", {
"gridScreenName": type,
"itemNames": names,
}));
Object.assign(results, countResult);
}
if (retry && itemList.some(item => !(item in results))) {
// 即使在白天,大多数情况也能识别成功,因此不作为常态机制,仅在失败时使用
log.info("部分物品识别失败,调整时间和视角后重试");
await genshin.returnMainUi();
await genshin.setTime(0, 0);
await sleep(300);
moveMouseBy(0, 9280);
await sleep(300);
const retryResults = await getItemCount(itemList, false);
await genshin.returnMainUi();
keyPress("MBUTTON");
return retryResults;
}
const finalResults = {};
for (const itemName of itemList) {
const normalizedName = renameMap[itemName] || itemName;
// 如果某个元素没有找到,则不会存在对应的键值,赋值为-1以保持与单个物品查找时一致的行为
finalResults[itemName] = results[normalizedName] ?? -1;
}
return finalResults;
}
function getItemCD(itemName) {
if (Object.keys(materialMetadata).length === 0) {
Object.assign(materialMetadata, parseCsvTextToDict());
}
return materialMetadata[itemName]?.cd || null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
const defaultReplacementMap = {
: "盐",
: "卯",
};
/**
* 绘制方框以标记OCR区域
*
* @param {number[4] | {x: number, y: number, width: number, height: number}} ocrRegion - OCR 区域,数组或对象形式,表示 [x, y, width, height]
* @param {object|null} [captureRegion=null] - 截图区域对象。为 null 时自动调用 captureGameRegion()
* @throws {TypeError} 当 ocrRegion 既不是数组也不是对象时抛出。
*/
function drawOcrRegion(ocrRegion, captureRegion = null) {
let x, y, width, height;
if (Array.isArray(ocrRegion)) {
[x, y, width, height] = ocrRegion;
} else if (typeof ocrRegion === "object" && ocrRegion !== null) {
({ x, y, width, height } = ocrRegion);
} else {
throw new TypeError("'ocrRegion' must be an array [x, y, width, height] or an object with x, y, width, height properties");
}
let auto_created = false;
if (captureRegion === null) {
captureRegion = captureGameRegion();
auto_created = true;
}
let region = captureRegion.DeriveCrop(x, y, width, height);
let name = [x, y, width, height].toString();
region.DrawSelf(name);
region.dispose();
if (auto_created) {
captureRegion.dispose();
}
}
async function getTextInRegion(ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) {
let x, y, width, height;
if (Array.isArray(ocrRegion)) {
[x, y, width, height] = ocrRegion;
} else if (typeof ocrRegion === "object" && ocrRegion !== null) {
({ x, y, width, height } = ocrRegion);
} else {
throw new Error("Invalid parameter 'ocrRegion'");
}
const debugThreshold = timeout / retryInterval / 3;
let startTime = Date.now();
let retryCount = 0; // 重试计数
while (Date.now() - startTime < timeout) {
let captureRegion = captureGameRegion();
try {
// 尝试 OCR 识别
let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域
// 遍历识别结果,检查是否找到目标文本
for (let res of resList) {
// 后处理:根据替换映射表检查和替换错误识别的字符
let correctedText = res.text;
for (let [wrongChar, correctChar] of Object.entries(replacementMap)) {
correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar);
}
captureRegion.dispose();
return correctedText.trim();
}
} catch (error) {
log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`);
}
retryCount++; // 增加重试计数
if (retryCount > debugThreshold) {
let region = captureRegion.DeriveCrop(x, y, width, height);
region.DrawSelf("debug");
region.dispose();
}
captureRegion.dispose();
await sleep(retryInterval);
}
return null;
}
async function waitForTextAppear(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) {
let x, y, width, height;
if (Array.isArray(ocrRegion)) {
[x, y, width, height] = ocrRegion;
} else if (typeof ocrRegion === "object" && ocrRegion !== null) {
({ x, y, width, height } = ocrRegion);
} else {
throw new Error("Invalid parameter 'ocrRegion'");
}
const debugThreshold = timeout / retryInterval / 3;
let startTime = Date.now();
let retryCount = 0; // 重试计数
while (Date.now() - startTime < timeout) {
let captureRegion = captureGameRegion();
try {
// 尝试 OCR 识别
let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域
// 遍历识别结果,检查是否找到目标文本
for (let res of resList) {
// 后处理:根据替换映射表检查和替换错误识别的字符
let correctedText = res.text;
for (let [wrongChar, correctChar] of Object.entries(replacementMap)) {
correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar);
}
if (correctedText.includes(targetText)) {
captureRegion.dispose();
return { success: true, wait_time: Date.now() - startTime };
}
}
} catch (error) {
log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`);
}
retryCount++; // 增加重试计数
if (retryCount > debugThreshold) {
let region = captureRegion.DeriveCrop(x, y, width, height);
region.DrawSelf("debug");
region.dispose();
}
captureRegion.dispose();
await sleep(retryInterval);
}
return { success: false };
}
async function recognizeTextAndClick(targetText, ocrRegion, timeout = 5000, retryInterval = 50, replacementMap = defaultReplacementMap) {
let x, y, width, height;
if (Array.isArray(ocrRegion)) {
[x, y, width, height] = ocrRegion;
} else if (typeof ocrRegion === "object" && ocrRegion !== null) {
({ x, y, width, height } = ocrRegion);
} else {
throw new Error("Invalid parameter 'ocrRegion'");
}
const debugThreshold = timeout / retryInterval / 3;
let startTime = Date.now();
let retryCount = 0; // 重试计数
while (Date.now() - startTime < timeout) {
let captureRegion = captureGameRegion();
try {
// 尝试 OCR 识别
let resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, width, height)); // 指定识别区域
// 遍历识别结果,检查是否找到目标文本
for (let res of resList) {
// 后处理:根据替换映射表检查和替换错误识别的字符
let correctedText = res.text;
for (let [wrongChar, correctChar] of Object.entries(replacementMap)) {
correctedText = correctedText.replace(new RegExp(wrongChar, "g"), correctChar);
}
if (correctedText.includes(targetText)) {
// 如果找到目标文本,计算并点击文字的中心坐标
let centerX = Math.round(res.x + res.width / 2);
let centerY = Math.round(res.y + res.height / 2);
await click(centerX, centerY);
await sleep(50);
captureRegion.dispose();
return { success: true, x: centerX, y: centerY };
}
}
} catch (error) {
log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`);
}
retryCount++; // 增加重试计数
if (retryCount > debugThreshold) {
let region = captureRegion.DeriveCrop(x, y, width, height);
region.DrawSelf("debug");
region.dispose();
}
captureRegion.dispose();
await sleep(retryInterval);
}
return { success: false };
}
async function isTextExistedInRegion(searchText, ocrRegion) {
let x, y, width, height;
if (Array.isArray(ocrRegion)) {
[x, y, width, height] = ocrRegion;
} else if (typeof ocrRegion === "object" && ocrRegion !== null) {
({ x, y, width, height } = ocrRegion);
} else {
throw new Error("Invalid parameter 'ocrRegion'");
}
let captureRegion = captureGameRegion();
const result = captureRegion.find(RecognitionObject.ocr(x, y, width, height));
captureRegion.dispose();
if (result.text) {
if (typeof searchText === "string") {
return result.text.includes(searchText);
} else if (searchText instanceof RegExp) {
return result.text.match(searchText);
}
}
return false;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,21 @@
{
"manifest_version": 1,
"name": "带CD管理的自动采集",
"version": "1.7.9",
"bgi_version": "0.45.0",
"description": "自动同步你通过BetterGI订阅的地图追踪任务执行采集任务并管理材料刷新时间(支持多账号。\n首次使用前请先简单阅读说明(可在`全自动`——`JS脚本`页面,点击本脚本名称查看)",
"name": "带CD管理和目标设定的自动采集",
"version": "2.0",
"bgi_version": "0.54.1-alpha.1",
"description": "自动同步你通过BetterGI订阅的地图追踪任务执行采集任务并管理材料采集目标和刷新时间。\n支持联机模式采集和多账号记录。\n首次使用前请先简单阅读说明",
"authors": [
{
"name": "Ayaka-Main",
"links": "https://github.com/Patrick-Ze"
},
{
"name": "蜜柑魚",
"links": "https://github.com/this-Fish"
}
],
"settings_ui": "settings.json",
"main": "main.js"
"main": "main.js",
"saved_files": [
"pathing",
"record/*.csv",
"record/*.txt",
"record/*.json"
]
}

View File

@@ -1,48 +1,95 @@
[
{
"name": "runMode",
"type": "select",
"label": "运行模式",
"options": [
"扫描文件夹更新可选材料列表",
"采集选中的材料",
"清除运行记录(重置材料刷新时间)"
]
},
{
"name": "subscribeMode",
"type": "select",
"label": "'地图追踪'中已订阅的任务目录的处理方式:",
"options": [
"每次自动扫描,并采集扫描到的所有材料",
"手动扫描,并采集扫描到的所有材料",
"手动扫描,只采集已勾选的材料"
]
},
{
"name": "partyName",
"type": "input-text",
"label": "设置首选队伍名称(留空则不进行切换)"
},
{
"name": "partyName2nd",
"type": "input-text",
"label": "设置备选队伍名称(首选队伍缺少对应的采集角色时使用)"
},
{
"name": "stopAtTime",
"type": "input-text",
"label": "停止运行时间HH:mm24小时制。例如09:28为上午9点28分"
},
{
"name": "iHaveMultipleAccounts",
"type": "input-text",
"label": "我肝的账号不止一个\n根据唯一ID区分账号维护对应的材料刷新时间\n留空时将使用右下角UID",
"default": "默认账号"
},
{
"name": "acceptMultiplePathOfSameMaterial",
"type": "checkbox",
"label": "即使同一种材料有多个版本的路线,也全都执行采集"
}
[
{
"name": "runMode",
"type": "select",
"label": "运行模式",
"options": [
"扫描文件夹更新可选材料列表",
"采集选中的材料"
],
"default": "扫描文件夹更新可选材料列表"
},
{
"name": "filterPathByKeywords",
"type": "input-text",
"label": "按关键词筛选材料路线(使用空格分隔,默认为黑名单模式)",
"default": "低成功率 不建议"
},
{
"name": "partyName",
"type": "input-text",
"label": "设置首选队伍名称(留空则不进行切换)"
},
{
"name": "partyName2nd",
"type": "input-text",
"label": "设置备选队伍名称(首选队伍缺少对应的采集角色时使用)"
},
{
"name": "excludeTimeRange",
"type": "input-text",
"label": "不在以下时间段内运行HH:mm-HH:mm24小时制"
},
{
"name": "targetCountOfSelected",
"type": "input-text",
"label": "物品采集数量目标可使用csv为不同物品设置不同数量"
},
{
"name": "manualSetAccountName",
"type": "input-text",
"label": "使用指定的账号名保存运行记录"
},
{
"type": "separator"
},
{
"label": "\n【常用采集选项】\n\n按大类选择",
"type": "multi-checkbox",
"name": "selectByCategory",
"options": [
"地方特产",
"矿物",
"食材与炼金"
]
},
{
"label": "按地区选择地方特产",
"type": "multi-checkbox",
"name": "selectLocalSpecialtyByCountry",
"options": [
"蒙德",
"璃月",
"稻妻",
"须弥",
"枫丹",
"纳塔",
"挪德卡莱"
]
},
{
"label": "矿物",
"type": "multi-checkbox",
"name": "selectForgingOre",
"options": [
"白铁块",
"水晶块",
"星银矿石",
"紫晶块",
"萃凝晶",
"虹滴晶"
]
},
{
"label": "特殊物品",
"type": "multi-checkbox",
"name": "selectMiscellaneous",
"options": [
"晶蝶",
"沉玉仙茗"
]
},
{
"type": "separator"
}
]

View File

@@ -1,48 +0,0 @@
[
{
"name": "runMode",
"type": "select",
"label": "运行模式",
"options": [
"扫描文件夹更新可选材料列表",
"采集选中的材料",
"清除运行记录(重置材料刷新时间)"
]
},
{
"name": "subscribeMode",
"type": "select",
"label": "'地图追踪'中已订阅的任务目录的处理方式:",
"options": [
"每次自动扫描,并采集扫描到的所有材料",
"手动扫描,并采集扫描到的所有材料",
"手动扫描,只采集已勾选的材料"
]
},
{
"name": "partyName",
"type": "input-text",
"label": "设置首选队伍名称(留空则不进行切换)"
},
{
"name": "partyName2nd",
"type": "input-text",
"label": "设置备选队伍名称(首选队伍缺少对应的采集角色时使用)"
},
{
"name": "stopAtTime",
"type": "input-text",
"label": "停止运行时间HH:mm24小时制。例如09:28为上午9点28分"
},
{
"name": "iHaveMultipleAccounts",
"type": "input-text",
"label": "我肝的账号不止一个\n根据唯一ID区分账号维护对应的材料刷新时间\n填空将使用右下角帐号UID区分账号维护对应的材料刷新时间",
"default": "默认账号"
},
{
"name": "acceptMultiplePathOfSameMaterial",
"type": "checkbox",
"label": "即使同一种材料有多个版本的路线,也全都执行采集"
}
]

View File

@@ -1,44 +0,0 @@
提供一个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. 重置运行记录中的刷新时间为绫华生日