├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── nonebot_plugin_arktools
├── __init__.py
├── data
│ ├── arknights
│ │ └── processed_data
│ │ │ └── nicknames.json
│ ├── fonts
│ │ ├── Arknights-en.ttf
│ │ └── Arknights-zh.otf
│ └── guess_character
│ │ ├── correct.png
│ │ ├── down.png
│ │ ├── up.png
│ │ ├── vague.png
│ │ └── wrong.png
├── src
│ ├── __init__.py
│ ├── configs
│ │ ├── __init__.py
│ │ ├── draw_config.py
│ │ ├── ocr_config.py
│ │ ├── path_config.py
│ │ ├── proxy_config.py
│ │ └── scheduler_config.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── database
│ │ │ ├── __init__.py
│ │ │ ├── game_sqlite.py
│ │ │ └── plugin_sqlite.py
│ │ └── models_v3.py
│ ├── exceptions
│ │ └── __init__.py
│ ├── game_draw_card
│ │ └── __init__.py
│ ├── game_guess_operator
│ │ ├── __init__.py
│ │ └── data_source.py
│ ├── help.py
│ ├── misc_monster_siren
│ │ ├── __init__.py
│ │ └── data_source.py
│ ├── misc_operator_birthday
│ │ └── __init__.py
│ ├── tool_announce_push
│ │ ├── __init__.py
│ │ ├── data_source.py
│ │ └── groups.txt
│ ├── tool_fetch_maa_copilot
│ │ ├── __init__.py
│ │ └── data_source.py
│ ├── tool_open_recruitment
│ │ ├── __init__.py
│ │ └── data_source.py
│ ├── tool_operator_info
│ │ ├── __init__.py
│ │ └── data_source.py
│ ├── tool_sanity_notify
│ │ └── __init__.py
│ └── utils
│ │ ├── __init__.py
│ │ ├── database.py
│ │ ├── general.py
│ │ ├── image.py
│ │ └── update.py
└── test
│ ├── __init__.py
│ ├── test_database_utils.py
│ ├── test_models.py
│ ├── test_open_recruitment.py
│ ├── test_operator_info.py
│ ├── test_update_utils.py
│ └── utils.py
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.xml
3 | *.iml
4 | *.zip
5 | *.pyc
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Number_Sir
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Nonebot_Plugin_ArkTools
8 |
9 | _✨ 基于 OneBot 适配器的 [NoneBot2](https://v2.nonebot.dev/) 明日方舟小工具箱插件 ✨_
10 |
11 |
12 |
13 | [](https://www.oscs1024.com/project/NumberSir/nonebot_plugin_arktools?ref=badge_small) [](https://gitee.com/Number_Sir/nonebot_plugin_arktools/stargazers)
14 |
15 | 本人python小萌新,插件有不完善和可以改进之处欢迎各位多提pr和issue
16 |
17 | - [功能](#功能)
18 | - [安装](#安装)
19 | - [使用](#如何使用)
20 | - [示例](#图片示例)
21 | - [感谢](#感谢)
22 | - [更新日志](#更新日志)
23 |
24 | # 功能
25 | ## 已实现:
26 | 1. [x] 可以查询推荐的公招标签(截图识别/手动输文字)
27 | 2. [x] 可以查询干员的技能升级材料、专精材料、精英化材料、模组升级材料
28 | 3. [x] 可以通过网易云点歌,以卡片形式发送
29 | 4. [x] 猜干员小游戏,玩法与 [wordle](https://github.com/noneplugin/nonebot-plugin-wordle) 相同
30 | 5. [x] 可以查看生日为今天的干员
31 | 6. [x] 可以记录当前理智,等回复满后提醒
32 | 7. [x] 指定群聊自动推送最新游戏公告
33 | 8. [x] 查询、订阅、推送 [MAA 作业站](https://prts.plus)的作业
34 |
35 | ## 编写中...
36 | 1. [ ] 可以查询某种资源在哪个关卡期望理智最低
37 | 2. [ ] 根据当前有的资源和需要的资源种类、数量测算最优推图计划
38 | 3. [ ] 查询某干员的基础数据:
39 | 1. [ ] 给定等级、信赖、潜能下的基础面板
40 | 2. [ ] 天赋、特性、技能
41 | 3. [ ] 干员种族、势力、身高等基本个人信息
42 | 4. [ ] 定时提醒剿灭 / 蚀刻章 / 合约等活动过期
43 |
44 | # 安装
45 | - 使用 pip
46 | ```
47 | pip install -U nonebot_plugin_arktools
48 | ```
49 |
50 | - 使用 nb-cli
51 | ```
52 | nb plugin install nonebot_plugin_arktools
53 | ```
54 |
55 | # 如何使用
56 | ## 启动注意
57 | - 每次启动并连接到客户端后会从 __[明日方舟常用素材库](https://github.com/yuanyan3060/Arknights-Bot-Resource)__(__[yuanyan3060](https://github.com/yuanyan3060)__), __[《明日方舟》游戏数据库](https://github.com/Kengxxiao/ArknightsGameData)__(__[Kengxxiao](https://github.com/Kengxxiao)__), __[Arknight-Images](https://github.com/Aceship/Arknight-Images)__(__[Aceship](https://github.com/Aceship)__) 下载使用插件必需的文本及图片资源到本地,已经下载过的文件不会重复下载。下载根据网络情况不同可能耗时 5 分钟左右
58 | - 如需手动更新,请用命令 __“更新方舟素材”__ 进行更新
59 | - 如果自动下载失败,请手动下载发行版中的 __“`data.zip`”/“`data.tar.gz`”__ 压缩文件,解压到 “`机器人根目录`” 文件夹下(即运行 `nb run` 命令的文件夹/ `bot.py` 的文件夹)。正确放置的文件夹结构应为:
60 | ```txt
61 | 举例:
62 | ├── data
63 | │ └── arktools
64 | │ ├── arknights
65 | │ │ ├── gamedata
66 | │ │ │ └── excel
67 | │ │ │ └── ...
68 | │ │ ├── gameimage
69 | │ │ │ └── ...
70 | │ │ ├── processed_data
71 | │ │ │ └── nicknames.json
72 | │ │ └── ...
73 | │ ├── fonts
74 | │ │ ├── Arknights-en.ttf
75 | │ │ └── Arknights-zh.otf
76 | │ ├── guess_character
77 | │ │ ├── correct.png
78 | │ │ ├── down.png
79 | │ │ ├── up.png
80 | │ │ ├── vague.png
81 | │ │ └── wrong.png
82 | │ └── ...
83 | ├── plugin
84 | │ └── nonebot_plugin_arktools
85 | │ ├── src
86 | │ └── ...
87 | ├── .env
88 | ├── .env.dev
89 | ├── .env.prod
90 | ...
91 | ```
92 |
93 | ## .env.env 配置项
94 |
95 | ```ini
96 | # 百度 OCR 配置,公招识别截图用
97 | # 具体见 https://console.bce.baidu.com/ai/?fromai=1#/ai/ocr/app/list
98 | ARKNIGHTS_BAIDU_API_KEY="xxx" # 【必填】百度 OCR API KEY
99 | ARKNIGHTS_BAIDU_SECRET_KEY="xxx" # 【必填】百度 OCR SECRET KEY
100 |
101 | # 代理配置,如部署机器人的服务器在国内大陆地区可能需要修改
102 | GITHUB_RAW="https://raw.githubusercontent.com" # 默认为 https://raw.githubusercontent.com,如有镜像源可以替换,如 https://ghproxy.com/https://raw.githubusercontent.com
103 | GITHUB_SITE="https://github.com" # 默认为 https://github.com,如有镜像源可以替换,如 https://kgithub.com
104 | RSS_SITE="https://rsshub.app" # 默认为 https://rsshub.app,如有镜像源可以替换
105 |
106 | # 定时任务配置,默认是关闭的
107 | ANNOUNCE_PUSH_SWITCH=False # 是否自动推送舟舟最新公告,默认为 False; True 为开启自动检测
108 | ANNOUNCE_PUSH_INTERVAL=1 # 自动推送最新公告的检测间隔,上述开关开启时有效,默认为 1 分钟
109 | SANITY_NOTIFY_SWITCH=False # 是否自动检测理智提醒,默认为 False; True 为开启自动检测
110 | SANITY_NOTIFY_INTERVAL=10 # 自动检测理智提醒的检测间隔,上述开关开启时有效,默认为 10 分钟
111 | MAA_COPILOT_SWITCH=False # 是否自动推送MAA作业站新作业,默认为 False; True 为开启自动检测
112 | MAA_COPILOT_INTERVAL=60 # 自动推送MAA作业站新作业的检测间隔,上述开关开启时有效,默认为 60 分钟
113 |
114 | # 启动前素材检查配置,默认是开启的
115 | ARKNIGHTS_UPDATE_CHECK_SWITCH=True # 是否在启动bot时检查素材版本并下载,默认为True; False 为禁用检查
116 |
117 | # 资源路径配置,默认在启动机器人的目录中/运行nb run的目录中/放bot.py的目录中
118 | ARKNIGHTS_DATA_PATH="data/arktools" # 资源根路径,如果修改了根路径,下方路径都要修改
119 | ARKNIGHTS_FONT_PATH="data/arktools/fonts" # 字体路径
120 | ARKNIGHTS_GAMEDATA_PATH="data/arktools/arknights/gamedata" # 游戏数据
121 | ARKNIGHTS_GAMEIMAGE_PATH="data/arktools/arknights/gameimage" # 游戏图像
122 | ARKNIGHTS_DB_URL="data/arktools/databases/arknights_sqlite.sqlite3" # 数据库
123 |
124 | ...
125 | ```
126 | 各配置项的含义如上。
127 |
128 |

129 |
130 |
131 |
132 | ## 干员昵称
133 | 位置默认在 `data/arknights/processed_data/nicknames.json` 键为干员中文名称,值为昵称,可自行修改。
134 |
135 | ## 指令
136 |
137 | 点击展开
138 |
139 | ### 详细指令
140 | 使用以下指令触发,需加上指令前缀
141 | ```text
142 | 格式:
143 | 指令 => 含义
144 | [] 代表参数
145 | xxx/yyy 代表 xxx 或 yyy
146 | ```
147 | 杂项
148 | ```text
149 | 方舟帮助 / arkhelp => 查看指令列表
150 | 更新方舟素材 => 手动更新游戏数据(json)与图片
151 | 更新方舟数据库 => 手动更新数据库
152 | 更新方舟数据库 -D => 删除原数据库各表并重新写入
153 | ```
154 | 猜干员
155 | ```text
156 | 猜干员 => 开始新游戏
157 | #[干员名] => 猜干员,如:#艾雅法拉
158 | 提示 => 查看答案干员的信息
159 | 结束 => 结束当前局游戏
160 | ```
161 | 今日干员
162 | ```text
163 | 今日干员 => 查看今天过生日的干员
164 | ```
165 | 塞壬点歌
166 | ```text
167 | 塞壬点歌 [关键字] => 网易云点歌,以卡片形式发到群内
168 | ```
169 | 干员信息
170 | ```text
171 | 干员 [干员名] => 查看干员的精英化、技能升级、技能专精、模组解锁需要的材料
172 | ```
173 | 公开招募
174 | ```text
175 | 公招 [公招界面截图] => 查看标签组合及可能出现的干员
176 | 回复截图:公招 => 同上
177 | 公招 [标签1] [标签2] ... => 同上
178 | ```
179 | 理智提醒
180 | ```text
181 | 理智提醒 => 默认记当前理智为0,回满到135时提醒"
182 | 理智提醒 [当前理智] [回满理智] => 同上,不过手动指定当前理智与回满理智"
183 | 理智查看 => 查看距离理智回满还有多久,以及当期理智为多少"
184 | ```
185 | 公告推送
186 | ```text
187 | 添加方舟推送群 / ADDGROUP => 添加自动推送的群号
188 | 删除方舟推送群 / DELGROUP => 删除自动推送的群号
189 | 查看方舟推送群 / GETGROUP => 查看自动推送的群号
190 | ```
191 | MAA 作业站相关
192 | ```text
193 | maa添加订阅 / ADDMAA [关键词1 关键词2 ...] => 添加自动推送的关键词
194 | maa删除订阅 / DELMAA [关键词1 关键词2 ...] => 删除自动推送的关键词
195 | maa查看订阅 / GETMAA => 查看本群自动推送的关键词
196 |
197 | maa查作业 [关键词1 关键词2 ...] => 按关键词组合查作业,默认为最新发布的第一个作业
198 | maa查作业 [关键词1 关键词2 ...] | [热度/最新/访问] => 同上,不过可以指定按什么顺序查询
199 | ```
200 |
201 |
202 | # 图片示例
203 |
204 | 点击展开
205 |
206 | ## 图片们
207 |
208 |

209 |
210 |
211 |
212 |

213 |
214 |
215 |
216 |

217 |
218 |
219 |
220 |

221 |
222 |
223 |
224 |

225 |
226 |
227 |
228 |

229 |
230 |
231 |
232 |

233 |
234 |
235 |
236 |

237 |
238 |
239 |
240 |

241 |
242 |
243 |
244 |

245 |
246 |
247 |
248 |

249 |
250 |
251 |
252 |

253 |
254 |
255 |
256 |

257 |
258 |
259 |
260 |
261 | # 感谢
262 | - __[yuanyan3060](https://github.com/yuanyan3060)__ 的 __[明日方舟常用素材库](https://github.com/yuanyan3060/Arknights-Bot-Resource)__
263 | - __[Aceship](https://github.com/Aceship)__ 的 __[Arknight-Images](https://github.com/Aceship/Arknight-Images)__
264 | - __[AmiyaBot](https://github.com/AmiyaBot)__ 的 __[Amiya-bot](https://github.com/AmiyaBot/Amiya-Bot)__
265 | - __[Strelizia02](https://github.com/Strelizia02)__ 的 __[AngelinaBot](https://github.com/Strelizia02/AngelinaBot)__
266 | - __[MaaAssistantArknights](https://github.com/MaaAssistantArknights)__ 的 __[MAA](https://github.com/MaaAssistantArknights/MaaAssistantArknights)__
267 |
268 | # 更新日志
269 |
270 | 点击展开
271 |
272 | > 2023-05-04 v1.2.0
273 | > - 更换数据源 [@issue/42](https://github.com/NumberSir/nonebot_plugin_arktools/issues/42)
274 | > - 更新数据键值对
275 | > - 修复了使用 `ghproxy` 作为 github 镜像时无法获取数据的问题
276 | > - 添加了删表重写功能
277 | > - 修复了从 maa 作业站自动推送作业出错的问题
278 | >
279 | > 2023-04-15 v1.1.0
280 | > - 公招查询、猜干员、理智提醒现在均可以私聊进行 (不推荐,私聊发消息可能导致风控)
281 | > - 简易修复了与其它同用 Tortoise-ORM 的插件初始化冲突的问题 [@zx-issue/15](https://github.com/NumberSir/zhenxun_arktools/issues/15)
282 | > - 添加在群聊查询、订阅、推送 [MAA 作业站](https://prts.plus)作业的功能
283 | > - 修复了更新数据库中某张表格时会删除所有表格的问题
284 | >
285 | > 2023-04-08 v1.0.20
286 | > - 修复因素材库更新滞后导致无法查看干员的问题
287 | >
288 | > 2023-04-07 v1.0.19
289 | > - 修复更新数据库命令不会强制覆盖更新的问题
290 | >
291 | > 2023-04-06 v1.0.18
292 | > - 修复了舟舟更新数据结构导致的创建表单错误
293 | >
294 | > 2023-04-04 v1.0.17
295 | > - 添加数据库初始化检查,不再每次启动bot时重复创建
296 | > - 添加每次启动 bot 时的数据更新检查开关,默认启用 [@issue/39](https://github.com/NumberSir/nonebot_plugin_arktools/issues/39)
297 | >
298 | > 2023-03-28 v1.0.15
299 | > - 猜干员与干员信息功能可以使用干员昵称(可自行增删改查)
300 | >
301 | > 2023-03-24 v1.0.14
302 | > - 修复阿米娅与近卫阿米娅冲突的问题 [@zx-issue/13](https://github.com/NumberSir/zhenxun_arktools/issues/13)
303 | >
304 | > 2023-03-08 v1.0.12
305 | > - 添加 rsshub 代理配置项 [@issue/34](https://github.com/NumberSir/nonebot_plugin_arktools/issues/34)
306 | > - 修复公招命令不处理的问题 [@issue/35](https://github.com/NumberSir/nonebot_plugin_arktools/issues/35)
307 | > - 添加方舟素材/资源路径配置项,现在默认在机器人根目录下 `data/arktools` 文件夹 [@issue/36](https://github.com/NumberSir/nonebot_plugin_arktools/issues/36)
308 | > - 修复查询暮落干员信息时会选中空白暮落的问题
309 | >
310 | > 2023-02-20 v1.0.11
311 | > - 修复最新版本检测出错的问题
312 | >
313 | > 2023-02-19 v1.0.9
314 | > - 添加定时任务配置项
315 | > - 修复定时任务导致其它处理器阻塞的问题 [@issue/30](https://github.com/NumberSir/nonebot_plugin_arktools/issues/30) [@zx-issue/9](https://github.com/NumberSir/zhenxun_arktools/issues/9)
316 | > - 修复猜干员无法判断重复猜的问题 [@zx-issue/10](https://github.com/NumberSir/zhenxun_arktools/issues/10)
317 | > - 修复猜干员结果图不按顺序绘制的问题
318 | >
319 | > 2023-02-16 v1.0.8
320 | > - 移除 `nb plugin install` 安装命令,无法识别最新版本号 [@issue/28](https://github.com/NumberSir/nonebot_plugin_arktools/issues/28)
321 | > - 修改百度 OCR 配置项名称 [@issue/29](https://github.com/NumberSir/nonebot_plugin_arktools/issues/29)
322 | > - 修复资源下载与数据库初始化顺序不一致的问题
323 | > - 补充更多错误提示信息
324 | >
325 | > 2023-02-15 v1.0.7
326 | > - 添加自动推送最新公告功能 [@issue/10](https://github.com/NumberSir/nonebot_plugin_arktools/issues/10)
327 | > - 修复最新图像资源落后版本的问题
328 | > - 修复启动 nonebot 时不检查素材最新版本的问题
329 | >
330 | > 2023-02-13 v1.0.6
331 | > - 添加请求素材时的错误反馈
332 | >
333 | > 2023-02-13 v1.0.5
334 | > - 可替换 github 镜像源,原先的 kgithub.com 可能出现无法请求的问题[@issue/26](https://github.com/NumberSir/nonebot_plugin_arktools/issues/26)
335 | >
336 | > 2023-02-13 v1.0.3
337 | > - 重构插件目录结构
338 | > - 优化原有功能实现:干员信息、公招查询、理智提醒、塞壬点歌 [@issue/19](https://github.com/NumberSir/nonebot_plugin_arktools/issues/19) [@issue/21](https://github.com/NumberSir/nonebot_plugin_arktools/issues/21)
339 | > - 公招查询的截图识别改为 [百度 OCR](https://ai.baidu.com/tech/ocr) (腾讯 OCR 太拉了,识别不出烫金的高资和资深)
340 | > - 换用 [tortoise-orm](https://github.com/tortoise/tortoise-orm) 进行本地数据库异步读写
341 | > - 优化联网请求资源时的效率
342 | > - 添加新功能:猜干员、今日干员、帮助图片
343 | > - 最低支持 Python 版本上调至 Python3.8,与 Nonebot2-rc2 一致
344 | >
345 | > 2022-09-27 v0.5.8
346 | > - 修复理智恢复提醒文件检测不存在问题 [@issue/16](https://github.com/NumberSir/nonebot_plugin_arktools/issues/16)
347 | > - 重新添加文字公招查询 [@issue/17](https://github.com/NumberSir/nonebot_plugin_arktools/issues/17) [@issue/18](https://github.com/NumberSir/nonebot_plugin_arktools/issues/18)
348 | > - 优化干员查询:干员不存在时提醒
349 | > - 优化公招查询:反馈检测到的公招标签
350 | >
351 | > 2022-09-24 v0.5.7
352 | > - 修复干员公招查询算法问题 [@issue/13](https://github.com/NumberSir/nonebot_plugin_arktools/issues/13)
353 | > - 修复干员公招查询作图重叠问题
354 | > - 修复文件不存在报错问题 [@issue/15](https://github.com/NumberSir/nonebot_plugin_arktools/issues/15)
355 | > - 优化公招查询结果
356 | >
357 | > 2022-09-23 v0.5.6
358 | > - 干员查询添加模组材料查询
359 | >
360 | > 2022-09-15 v0.5.5
361 | > - 修复了json文件不会覆盖下载的问题
362 | > - 修复了公招识别读取头像路径的问题 [@issue/11](https://github.com/NumberSir/nonebot_plugin_arktools/issues/11)
363 | >
364 | > 2022-09-01 v0.5.4
365 | > - 修改资源获取方式为启动 nonebot 后下载到本地
366 | > - 修复了检测路径缺失的问题 [@issue/8](https://github.com/NumberSir/nonebot_plugin_arktools/issues/8)
367 | >
368 | > 2022-09-01 v0.5.3
369 | > - 修复未导入 os 模块的问题
370 | >
371 | > 2022-09-01 v0.5.2
372 | > - 修复公招保存图片出错和缺少文件的问题 [@issue/7](https://github.com/NumberSir/nonebot_plugin_arktools/issues/7)
373 | >
374 | > 2022-09-01 v0.5.1
375 | > - 重写了查询推荐公招标签的功能 [@issue/6](https://github.com/NumberSir/nonebot_plugin_arktools/issues/6)
376 | >
377 | > 2022-08-29 v0.5.0
378 | > - 添加了查询干员的技能升级材料、专精材料、精英化材料的功能
379 | >
380 | > 2022-06-03 v0.4.1
381 | > - 修复了发行版和源码不匹配的问题 [@issue/4](https://github.com/NumberSir/nonebot_plugin_arktools/issues/4)
382 | >
383 | > 2022-06-03 v0.4.0
384 | > - 添加了查询推荐公招标签的功能
385 | >
386 | > 2022-05-30 v0.3.0
387 | > - 向下兼容到 Python 3.7.3 版本 [@issue/2](https://github.com/NumberSir/nonebot_plugin_arktools/issues/2)
388 | >
389 | > 2022-05-30 v0.2.1
390 | > - 修复了使用 nb plugin install 命令安装后无法正常工作的问题 [@issue/1](https://github.com/NumberSir/nonebot_plugin_arktools/issues/1)
391 | >
392 | > 2022-05-26 v0.2.0
393 | > - 添加了查询最新活动信息的功能
394 | >
395 | > 2022-05-24 v0.1.0
396 | > - 添加了查询今日开放资源关卡的功能
397 |
398 |
399 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/__init__.py:
--------------------------------------------------------------------------------
1 | from .src import *
2 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/arknights/processed_data/nicknames.json:
--------------------------------------------------------------------------------
1 | {
2 | "Lancet-2": [
3 | "医疗小车",
4 | "医疗车"
5 | ],
6 | "Castle-3": [
7 | "近卫小车",
8 | "近卫车"
9 | ],
10 | "THRM-EX": [
11 | "特种小车",
12 | "特种车",
13 | "自爆小车",
14 | "自爆车"
15 | ],
16 | "正义骑士号": [
17 | "狙击小车",
18 | "狙击车"
19 | ],
20 | "泰拉大陆调查团": [
21 | "艾露猫"
22 | ],
23 | "夜刀": [],
24 | "黑角": [],
25 | "巡林者": [],
26 | "杜林": [],
27 | "12F": [
28 | "太子",
29 | "太子爷"
30 | ],
31 | "芬": [],
32 | "香草": [],
33 | "翎羽": [],
34 | "玫兰莎": [
35 | "剑圣",
36 | "玫剑圣"
37 | ],
38 | "泡普卡": [
39 | "鲍勃卡"
40 | ],
41 | "卡缇": [],
42 | "米格鲁": [],
43 | "斑点": [],
44 | "克洛丝": [
45 | "克天使",
46 | "kkdy",
47 | "酷酷队员",
48 | "扣扣打油"
49 | ],
50 | "安德切尔": [],
51 | "炎熔": [],
52 | "芙蓉": [],
53 | "安赛尔": [
54 | "猛男"
55 | ],
56 | "史都华德": [],
57 | "梓兰": [],
58 | "夜烟": [],
59 | "远山": [],
60 | "格雷伊": [],
61 | "卡达": [],
62 | "深靛": [],
63 | "布丁": [],
64 | "杰西卡": [
65 | "猫猫头",
66 | "流泪猫猫头"
67 | ],
68 | "流星": [],
69 | "红云": [],
70 | "梅": [],
71 | "白雪": [],
72 | "松果": [],
73 | "安比尔": [
74 | "挂B",
75 | "瓜天使",
76 | "茄子"
77 | ],
78 | "酸糖": [],
79 | "铅踝": [],
80 | "讯使": [],
81 | "清道夫": [],
82 | "红豆": [
83 | "赵子龙"
84 | ],
85 | "桃金娘": [
86 | "桃子",
87 | "小苹果"
88 | ],
89 | "豆苗": [],
90 | "杜宾": [],
91 | "缠丸": [],
92 | "断罪者": [],
93 | "霜叶": [],
94 | "艾丝黛尔": [
95 | "小鳄鱼"
96 | ],
97 | "慕斯": [],
98 | "刻刀": [],
99 | "宴": [],
100 | "芳汀": [],
101 | "罗小黑": [],
102 | "石英": [],
103 | "砾": [],
104 | "孑": [],
105 | "暗索": [],
106 | "末药": [],
107 | "嘉维尔": [],
108 | "苏苏洛": [],
109 | "调香师": [
110 | "调箱师"
111 | ],
112 | "清流": [],
113 | "褐果": [],
114 | "角峰": [],
115 | "蛇屠箱": [
116 | "龟龟"
117 | ],
118 | "泡泡": [],
119 | "古米": [],
120 | "坚雷": [],
121 | "深海色": [],
122 | "地灵": [],
123 | "波登可": [],
124 | "罗比菈塔": [],
125 | "伊桑": [],
126 | "阿消": [],
127 | "白面鸮": [
128 | "白咕咕",
129 | "白面咕"
130 | ],
131 | "微风": [],
132 | "凛冬": [
133 | "丢人熊",
134 | "冬将军"
135 | ],
136 | "德克萨斯": [
137 | "德狗"
138 | ],
139 | "贾维": [],
140 | "苇草": [],
141 | "野鬃": [],
142 | "极境": [
143 | "桃金郎",
144 | "大帅哥"
145 | ],
146 | "夜半": [],
147 | "晓歌": [],
148 | "谜图": [],
149 | "诗怀雅": [
150 | "吉祥物",
151 | "小老虎",
152 | "小脑斧",
153 | "叉烧猫"
154 | ],
155 | "鞭刃": [],
156 | "芙兰卡": [
157 | "胖狐狸"
158 | ],
159 | "炎客": [],
160 | "因陀罗": [],
161 | "燧石": [],
162 | "达格达": [],
163 | "拉普兰德": [
164 | "拉狗"
165 | ],
166 | "断崖": [],
167 | "铎铃": [],
168 | "柏喙": [],
169 | "战车": [],
170 | "幽灵鲨": [
171 | "鲨鲨"
172 | ],
173 | "布洛卡": [],
174 | "星极": [
175 | "D32钢",
176 | "D32"
177 | ],
178 | "铸铁": [],
179 | "赤冬": [
180 | "螃蟹"
181 | ],
182 | "火龙S黑角": [],
183 | "羽毛笔": [],
184 | "海沫": [],
185 | "龙舌兰": [],
186 | "蓝毒": [
187 | "蓝呱呱",
188 | "脆脆蛙"
189 | ],
190 | "白金": [
191 | "大位"
192 | ],
193 | "灰喉": [],
194 | "四月": [],
195 | "寒芒克洛丝": [
196 | "异克"
197 | ],
198 | "陨星": [],
199 | "慑砂": [],
200 | "截云": [],
201 | "送葬人": [
202 | "阿葬"
203 | ],
204 | "奥斯塔": [],
205 | "阿米娅": [
206 | "大女儿",
207 | "兔兔",
208 | "阿米兔"
209 | ],
210 | "苦艾": [],
211 | "特米米": [],
212 | "雪绒": [],
213 | "天火": [],
214 | "惊蛰": [
215 | "雷子姐"
216 | ],
217 | "星源": [],
218 | "蜜蜡": [],
219 | "莱恩哈特": [],
220 | "薄绿": [],
221 | "爱丽丝": [],
222 | "和弦": [],
223 | "炎狱炎熔": [],
224 | "蚀清": [],
225 | "耶拉": [],
226 | "洛洛": [],
227 | "至简": [],
228 | "梅尔": [],
229 | "稀音": [],
230 | "赫默": [
231 | "无人机"
232 | ],
233 | "华法琳": [
234 | "ff0"
235 | ],
236 | "亚叶": [],
237 | "锡兰": [],
238 | "絮雨": [],
239 | "图耶": [],
240 | "桑葚": [],
241 | "蜜莓": [],
242 | "濯尘芙蓉": [],
243 | "明椒": [],
244 | "临光": [],
245 | "吽": [],
246 | "红": [],
247 | "槐琥": [],
248 | "卡夫卡": [],
249 | "乌有": [],
250 | "雷蛇": [],
251 | "可颂": [
252 | "面包"
253 | ],
254 | "拜松": [],
255 | "火神": [],
256 | "石棉": [],
257 | "暮落": [],
258 | "车尔尼": [],
259 | "闪击": [
260 | "闪盾"
261 | ],
262 | "暴雨": [],
263 | "灰毫": [],
264 | "火哨": [],
265 | "极光": [],
266 | "普罗旺斯": [
267 | "大尾巴",
268 | "大尾巴狼"
269 | ],
270 | "守林人": [
271 | "茄子"
272 | ],
273 | "安哲拉": [
274 | "海哲拉",
275 | "海猫"
276 | ],
277 | "子月": [],
278 | "熔泉": [],
279 | "埃拉托": [],
280 | "承曦格雷伊": [],
281 | "崖心": [],
282 | "雪雉": [],
283 | "初雪": [],
284 | "巫恋": [],
285 | "真理": [
286 | "政委"
287 | ],
288 | "格劳克斯": [],
289 | "但书": [],
290 | "掠风": [],
291 | "空": [
292 | "爱豆",
293 | "爱抖露"
294 | ],
295 | "海蒂": [],
296 | "月禾": [],
297 | "九色鹿": [],
298 | "夏栎": [],
299 | "狮蝎": [],
300 | "绮良": [
301 | "kira",
302 | "Kira"
303 | ],
304 | "食铁兽": [
305 | "熊猫"
306 | ],
307 | "见行者": [],
308 | "罗宾": [],
309 | "霜华": [
310 | "夹子妹"
311 | ],
312 | "贝娜": [],
313 | "风丸": [],
314 | "能天使": [
315 | "阿能",
316 | "阿噗噜派",
317 | "小乐"
318 | ],
319 | "空弦": [],
320 | "灰烬": [
321 | "ash",
322 | "ASH",
323 | "Ash"
324 | ],
325 | "黑": [],
326 | "鸿雪": [],
327 | "远牙": [],
328 | "W": [],
329 | "菲亚梅塔": [
330 | "凤凰",
331 | "菲亚",
332 | "肥鸭"
333 | ],
334 | "早露": [
335 | "早子姐"
336 | ],
337 | "迷迭香": [
338 | "香香",
339 | "小女儿"
340 | ],
341 | "假日威龙陈": [
342 | "水陈"
343 | ],
344 | "推进之王": [
345 | "推王",
346 | "王小姐",
347 | "王维娜",
348 | "维娜"
349 | ],
350 | "风笛": [],
351 | "嵯峨": [
352 | "小僧"
353 | ],
354 | "琴柳": [
355 | "悲鸣",
356 | "贞德"
357 | ],
358 | "焰尾": [
359 | "团长"
360 | ],
361 | "伺夜": [],
362 | "伊芙利特": [
363 | "小火龙"
364 | ],
365 | "莫斯提马": [
366 | "蓝天使"
367 | ],
368 | "艾雅法拉": [
369 | "小羊"
370 | ],
371 | "刻俄柏": [
372 | "小刻",
373 | "氪二百"
374 | ],
375 | "夕": [],
376 | "异客": [
377 | "神"
378 | ],
379 | "卡涅利安": [
380 | "卡姐",
381 | "卡子姐"
382 | ],
383 | "林": [
384 | "小鼠王"
385 | ],
386 | "澄闪": [
387 | "粉毛",
388 | "猪猪",
389 | "佩奇"
390 | ],
391 | "黑键": [],
392 | "灵知": [],
393 | "安洁莉娜": [
394 | "杰哥",
395 | "洁哥",
396 | "安洁",
397 | "jk",
398 | "JK"
399 | ],
400 | "铃兰": [],
401 | "麦哲伦": [
402 | "麦麦",
403 | "哥伦布",
404 | "麦迪文",
405 | "小企鹅"
406 | ],
407 | "浊心斯卡蒂": [
408 | "浊蒂",
409 | "红蒂"
410 | ],
411 | "令": [
412 | "大姐"
413 | ],
414 | "白铁": [],
415 | "傀影": [],
416 | "缄默德克萨斯": [
417 | "张飞",
418 | "异德",
419 | "翼德"
420 | ],
421 | "麒麟X夜刀": [],
422 | "老鲤": [],
423 | "温蒂": [
424 | "弱女子"
425 | ],
426 | "阿": [],
427 | "歌蕾蒂娅": [
428 | "歌蒂"
429 | ],
430 | "水月": [
431 | "水母"
432 | ],
433 | "归溟幽灵鲨": [
434 | "归鲨"
435 | ],
436 | "多萝西": [],
437 | "闪灵": [
438 | "黑恶魔"
439 | ],
440 | "夜莺": [
441 | "白恶魔"
442 | ],
443 | "凯尔希": [
444 | "太后",
445 | "凯太后",
446 | "老女人"
447 | ],
448 | "流明": [],
449 | "焰影苇草": [
450 | "焰苇"
451 | ],
452 | "星熊": [
453 | "鬼姐"
454 | ],
455 | "塞雷娅": [
456 | "塞爹",
457 | "塞妈",
458 | "拳皇",
459 | "莱茵拳皇",
460 | "塞主任"
461 | ],
462 | "瑕光": [],
463 | "年": [],
464 | "泥岩": [],
465 | "斥罪": [],
466 | "森蚺": [],
467 | "号角": [],
468 | "山": [],
469 | "重岳": [
470 | "大哥"
471 | ],
472 | "银灰": [
473 | "前夫",
474 | "前夫哥",
475 | "丹增",
476 | "老板",
477 | "银老板",
478 | "盟友"
479 | ],
480 | "棘刺": [
481 | "海胆",
482 | "刺棘",
483 | "鸡翅"
484 | ],
485 | "仇白": [],
486 | "陈": [
487 | "粉肠龙",
488 | "肠粉龙"
489 | ],
490 | "艾丽妮": [
491 | "小鸟"
492 | ],
493 | "煌": [],
494 | "百炼嘉维尔": [
495 | "百嘉",
496 | "加百列",
497 | "嘉百列"
498 | ],
499 | "史尔特尔": [
500 | "42",
501 | "42姐",
502 | "42奶奶"
503 | ],
504 | "赫拉格": [
505 | "老爷子",
506 | "大公",
507 | "将军"
508 | ],
509 | "帕拉斯": [
510 | "牛牛"
511 | ],
512 | "耀骑士临光": [
513 | "耀光"
514 | ],
515 | "玛恩纳": [
516 | "老马",
517 | "老玛",
518 | "叔叔"
519 | ],
520 | "暴行": [],
521 | "空爆": [],
522 | "月见夜": [
523 | "牛郎"
524 | ],
525 | "猎蜂": [],
526 | "杰克": [],
527 | "夜魔": [],
528 | "格拉尼": [
529 | "小马"
530 | ],
531 | "斯卡蒂": [
532 | "蒂蒂",
533 | "蓝蒂"
534 | ],
535 | "缪尔赛思": [
536 | "缪缪"
537 | ],
538 | "霍尔海雅": [
539 | "蛇蛇"
540 | ]
541 | }
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/fonts/Arknights-en.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/fonts/Arknights-en.ttf
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/fonts/Arknights-zh.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/fonts/Arknights-zh.otf
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/guess_character/correct.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/guess_character/correct.png
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/guess_character/down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/guess_character/down.png
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/guess_character/up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/guess_character/up.png
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/guess_character/vague.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/guess_character/vague.png
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/data/guess_character/wrong.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/data/guess_character/wrong.png
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/__init__.py:
--------------------------------------------------------------------------------
1 | from .game_draw_card import * # 抽卡
2 | from .game_guess_operator import * # 猜干员
3 |
4 | from .misc_monster_siren import * # 点歌
5 | from .misc_operator_birthday import * # 生日提醒
6 |
7 | from .tool_announce_push import * # 新闻推送
8 | from .tool_fetch_maa_copilot import * # 抄作业
9 | from .tool_open_recruitment import * # 公招识别
10 | from .tool_operator_info import * # 干员信息
11 | from .tool_sanity_notify import * # 理智回复
12 |
13 | from .utils import *
14 | from .help import *
15 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/__init__.py:
--------------------------------------------------------------------------------
1 | from .path_config import *
2 | from .ocr_config import *
3 | from .proxy_config import *
4 | from .scheduler_config import *
5 | from .draw_config import *
6 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/draw_config.py:
--------------------------------------------------------------------------------
1 | """抽卡设置"""
2 | from pydantic import BaseModel, Extra
3 |
4 | class DrawConfig(BaseModel, extra=Extra.ignore):
5 | """干员概率"""
6 | draw_rate_6: float = 0.02
7 | draw_rate_5: float = 0.08
8 | draw_rate_4: float = 0.48
9 | draw_rate_3: float = 0.42
10 |
11 |
12 | __all__ = [
13 | "DrawConfig"
14 | ]
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/ocr_config.py:
--------------------------------------------------------------------------------
1 | """百度ocr设置"""
2 | from pydantic import BaseModel, Extra
3 |
4 |
5 | class BaiduOCRConfig(BaseModel, extra=Extra.ignore):
6 | """公招识别相关配置"""
7 | arknights_baidu_api_key: str = ""
8 | arknights_baidu_secret_key: str = ""
9 |
10 |
11 | __all__ = [
12 | "BaiduOCRConfig"
13 | ]
14 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/path_config.py:
--------------------------------------------------------------------------------
1 | """路径设置"""
2 | from pydantic import BaseModel, Extra
3 | from pathlib import Path
4 |
5 | # SRC_PATH = Path(__file__).absolute().parent.parent
6 |
7 |
8 | class PathConfig(BaseModel, extra=Extra.ignore):
9 | """游戏资源 & 数据库路径设置"""
10 | # arknights_data_path: Path = SRC_PATH.parent / "data"
11 | # arknights_font_path: Path = arknights_data_path / "fonts"
12 | # arknights_gamedata_path: Path = arknights_data_path / "arknights" / "gamedata"
13 | # arknights_gameimage_path: Path = arknights_data_path / "arknights" / "gameimage"
14 | # arknights_db_url: Path = arknights_data_path / "databases" / "arknights_sqlite.sqlite3"
15 |
16 | arknights_data_path: str = "data/arktools"
17 | arknights_font_path: str = "data/arktools/fonts"
18 | arknights_gamedata_path: str = "data/arktools/arknights/gamedata"
19 | arknights_gameimage_path: str = "data/arktools/arknights/gameimage"
20 | arknights_db_url: str = "data/arktools/databases/arknights_sqlite.sqlite3"
21 |
22 |
23 | __all__ = [
24 | "PathConfig"
25 | ]
26 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/proxy_config.py:
--------------------------------------------------------------------------------
1 | """代理设置"""
2 | from pydantic import BaseModel, Extra
3 |
4 |
5 | class ProxyConfig(BaseModel, extra=Extra.ignore):
6 | """github代理相关配置"""
7 | github_raw: str = "https://raw.githubusercontent.com" # 资源网址
8 | github_site: str = "https://github.com" # 访问网址
9 | rss_site: str = "https://rsshub.app"
10 |
11 |
12 | __all__ = [
13 | "ProxyConfig"
14 | ]
15 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/configs/scheduler_config.py:
--------------------------------------------------------------------------------
1 | """定时任务设置"""
2 | from pydantic import BaseModel, Extra
3 |
4 |
5 | class SchedulerConfig(BaseModel, extra=Extra.ignore):
6 | """定时任务相关配置"""
7 | announce_push_switch: bool = False # 自动获取 & 推送舟舟最新公告开关
8 | announce_push_interval: int = 1 # 间隔多少分钟运行一次
9 |
10 | sanity_notify_switch: bool = False # 检测理智提醒开关
11 | sanity_notify_interval: int = 10 # 间隔多少分钟运行一次
12 |
13 | arknights_update_check_switch: bool = True # 检测更新开关
14 |
15 | maa_copilot_switch: bool = False # 查询 MAA 作业站订阅内容开关
16 | maa_copilot_interval: int = 60 # 间隔多少分钟运行一次
17 |
18 |
19 | __all__ = [
20 | "SchedulerConfig"
21 | ]
22 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/core/__init__.py:
--------------------------------------------------------------------------------
1 | from .models_v3 import *
2 | from .database import *
3 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/core/database/__init__.py:
--------------------------------------------------------------------------------
1 | from .game_sqlite import *
2 | from .plugin_sqlite import *
3 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/core/database/game_sqlite.py:
--------------------------------------------------------------------------------
1 | """换用tortoise-orm了"""
2 | from tortoise.models import Model
3 | from tortoise import fields
4 |
5 |
6 | class BuildingBuffModel(Model):
7 | """后勤技能"""
8 | buffId = fields.CharField(null=True, max_length=255)
9 | buffName = fields.CharField(null=True, max_length=255)
10 | buffIcon = fields.CharField(null=True, max_length=255)
11 | skillIcon = fields.CharField(null=True, max_length=255)
12 | sortId = fields.IntField(null=True)
13 | buffColor = fields.CharField(null=True, max_length=255)
14 | textColor = fields.CharField(null=True, max_length=255)
15 | buffCategory = fields.CharField(null=True, max_length=255)
16 | roomType = fields.CharField(null=True, max_length=255)
17 | description = fields.CharField(null=True, max_length=1024)
18 |
19 | class Meta:
20 | table = "building_buff"
21 |
22 |
23 | class CharacterModel(Model):
24 | """干员"""
25 | charId = fields.CharField(null=True, max_length=255, description="干员代码")
26 | name = fields.CharField(null=True, max_length=255, description="干员中文名")
27 | description = fields.CharField(null=True, max_length=1024)
28 | canUseGeneralPotentialItem = fields.BooleanField(null=True)
29 | canUseActivityPotentialItem = fields.BooleanField(null=True)
30 | potentialItemId = fields.CharField(null=True, max_length=255)
31 | activityPotentialItemId = fields.CharField(null=True, max_length=255)
32 | classicPotentialItemId = fields.CharField(null=True, max_length=255)
33 | nationId = fields.CharField(null=True, max_length=255)
34 | groupId = fields.CharField(null=True, max_length=255)
35 | teamId = fields.CharField(null=True, max_length=255)
36 | displayNumber = fields.CharField(null=True, max_length=255)
37 | tokenKey = fields.CharField(null=True, max_length=255)
38 | appellation = fields.CharField(null=True, max_length=255, description="干员外文名")
39 | position = fields.CharField(null=True, max_length=255, description="高台/地面")
40 | tagList = fields.JSONField(null=True)
41 | itemUsage = fields.CharField(null=True, max_length=255)
42 | itemDesc = fields.CharField(null=True, max_length=1024)
43 | itemObtainApproach = fields.CharField(null=True, max_length=255)
44 | isNotObtainable = fields.BooleanField(null=True)
45 | isSpChar = fields.BooleanField(null=True)
46 | maxPotentialLevel = fields.IntField(null=True)
47 | rarity = fields.IntField(null=True)
48 | profession = fields.CharField(null=True, max_length=255)
49 | subProfessionId = fields.CharField(null=True, max_length=255)
50 | trait = fields.JSONField(null=True)
51 | phases = fields.JSONField(null=True)
52 | skills = fields.JSONField(null=True)
53 | talents = fields.JSONField(null=True)
54 | potentialRanks = fields.JSONField(null=True)
55 | favorKeyFrames = fields.JSONField(null=True)
56 | allSkillLvlup = fields.JSONField(null=True)
57 |
58 | class Meta:
59 | table = "character"
60 |
61 |
62 | class ConstanceModel(Model):
63 | """游戏常量"""
64 | maxLevel = fields.JSONField(null=True)
65 | characterExpMap = fields.JSONField(null=True)
66 | characterUpgradeCostMap = fields.JSONField(null=True)
67 | evolveGoldCost = fields.JSONField(null=True)
68 | attackMax = fields.FloatField(null=True)
69 | defMax = fields.FloatField(null=True)
70 | hpMax = fields.FloatField(null=True)
71 | reMax = fields.FloatField(null=True)
72 |
73 | class Meta:
74 | table = "constance"
75 |
76 |
77 | class EquipModel(Model):
78 | """模组"""
79 | uniEquipId = fields.CharField(null=True, max_length=255)
80 | uniEquipName = fields.CharField(null=True, max_length=255)
81 | uniEquipIcon = fields.CharField(null=True, max_length=255)
82 | uniEquipDesc = fields.CharField(null=True, max_length=2048)
83 | typeIcon = fields.CharField(null=True, max_length=255)
84 | typeName1 = fields.CharField(null=True, max_length=255)
85 | typeName2 = fields.CharField(null=True, max_length=255)
86 | equipShiningColor = fields.CharField(null=True, max_length=255)
87 | showEvolvePhase = fields.IntField(null=True)
88 | unlockEvolvePhase = fields.IntField(null=True)
89 | charId = fields.CharField(null=True, max_length=255)
90 | tmplId = fields.CharField(null=True, max_length=255)
91 | showLevel = fields.IntField(null=True)
92 | unlockLevel = fields.IntField(null=True)
93 | unlockFavorPoint = fields.IntField(null=True)
94 | missionList = fields.JSONField(null=True)
95 | itemCost = fields.JSONField(null=True)
96 | type = fields.CharField(null=True, max_length=255)
97 | uniEquipGetTime = fields.IntField(null=True)
98 | charEquipOrder = fields.IntField(null=True)
99 |
100 | character = fields.CharField(null=True, max_length=255)
101 |
102 | template = fields.CharField(null=True, max_length=255)
103 | desc = fields.CharField(null=True, max_length=1024)
104 | paramList = fields.JSONField(null=True)
105 | uniEquipMissionId = fields.CharField(null=True, max_length=255)
106 | uniEquipMissionSort = fields.IntField(null=True)
107 | jumpStageId = fields.CharField(null=True, max_length=255)
108 |
109 | class Meta:
110 | table = "equip"
111 |
112 |
113 | class HandbookInfoModel(Model):
114 | """档案"""
115 | infoId = fields.CharField(null=True, max_length=255)
116 | charID = fields.CharField(null=True, max_length=255)
117 | isLimited = fields.BooleanField(null=True)
118 | infoName = fields.CharField(null=True, max_length=255)
119 | storyTextAudio = fields.JSONField(null=True, max_length=2048)
120 | handbookAvgList = fields.JSONField(null=True, max_length=2048)
121 | sex = fields.CharField(null=True, max_length=255)
122 |
123 | class Meta:
124 | table = "handbook_info"
125 |
126 |
127 | class HandbookStageModel(Model):
128 | """悖论模拟"""
129 | charId = fields.CharField(null=True, max_length=255)
130 | stageId = fields.CharField(null=True, max_length=255)
131 | levelId = fields.CharField(null=True, max_length=255)
132 | zoneId = fields.CharField(null=True, max_length=255)
133 | code = fields.CharField(null=True, max_length=255)
134 | name = fields.CharField(null=True, max_length=255)
135 | loadingPicId = fields.CharField(null=True, max_length=255)
136 | description = fields.CharField(null=True, max_length=255)
137 | unlockParam = fields.JSONField(null=True)
138 | rewardItem = fields.JSONField(null=True)
139 | stageNameForShow = fields.CharField(null=True, max_length=255)
140 | zoneNameForShow = fields.CharField(null=True, max_length=255)
141 | picId = fields.CharField(null=True, max_length=255)
142 | stageGetTime = fields.BigIntField(null=True)
143 |
144 | class Meta:
145 | table = "handbook_stage"
146 |
147 |
148 | class ItemModel(Model):
149 | """物品"""
150 | itemId = fields.CharField(null=True, max_length=255)
151 | name = fields.CharField(null=True, max_length=255)
152 | description = fields.CharField(null=True, max_length=1024)
153 | rarity = fields.IntField(null=True)
154 | iconId = fields.CharField(null=True, max_length=255)
155 | overrideBkg = fields.CharField(null=True, max_length=255)
156 | stackIconId = fields.CharField(null=True, max_length=255)
157 | sortId = fields.IntField(null=True)
158 | usage = fields.CharField(null=True, max_length=255)
159 | obtainApproach = fields.CharField(null=True, max_length=255)
160 | classifyType = fields.CharField(null=True, max_length=255)
161 | itemType = fields.CharField(null=True, max_length=255)
162 | stageDropList = fields.JSONField(null=True)
163 | buildingProductList = fields.JSONField(null=True)
164 | hideInItemGet = fields.BooleanField(null=True)
165 |
166 | class Meta:
167 | table = "item"
168 |
169 |
170 | class RichTextStyleModel(Model):
171 | """文字样式"""
172 | text = fields.CharField(null=True, max_length=255)
173 | style = fields.CharField(null=True, max_length=255)
174 |
175 | class Meta:
176 | table = "rich_text_style"
177 |
178 |
179 | class SkillModel(Model):
180 | """技能"""
181 | skillId = fields.CharField(null=True, max_length=255)
182 | iconId = fields.CharField(null=True, max_length=255)
183 | hidden = fields.BooleanField(null=True)
184 | levels = fields.JSONField(null=True)
185 |
186 | name = fields.CharField(null=True, max_length=255)
187 | skillType = fields.IntField(null=True)
188 | durationType = fields.IntField(null=True)
189 | prefabId = fields.CharField(null=True, max_length=255)
190 |
191 | class Meta:
192 | table = "skill"
193 |
194 |
195 | class TermDescriptionModel(Model):
196 | """特殊状态"""
197 | termId = fields.CharField(null=True, max_length=255)
198 | termName = fields.CharField(null=True, max_length=255)
199 | description = fields.CharField(null=True, max_length=255)
200 |
201 | class Meta:
202 | table = "term_description"
203 |
204 |
205 | class WorkshopFormulaModel(Model):
206 | """制造站配方"""
207 | sortId = fields.IntField(null=True)
208 | formulaId = fields.CharField(null=True, max_length=255)
209 | rarity = fields.IntField(null=True)
210 | itemId = fields.CharField(null=True, max_length=255)
211 | count = fields.IntField(null=True)
212 | goldCost = fields.IntField(null=True)
213 | apCost = fields.IntField(null=True)
214 | formulaType = fields.CharField(null=True, max_length=255)
215 | buffType = fields.CharField(null=True, max_length=255)
216 | extraOutcomeRate = fields.FloatField(null=True)
217 | extraOutcomeGroup = fields.JSONField(null=True)
218 | costs = fields.JSONField(null=True)
219 | requireRooms = fields.JSONField(null=True)
220 | requireStages = fields.JSONField(null=True)
221 |
222 | class Meta:
223 | table = "workshop_formula"
224 |
225 |
226 | class GachaPoolModel(Model):
227 | """卡池"""
228 | gachaPoolId = fields.CharField(null=True, max_length=255)
229 | gachaIndex = fields.IntField(null=True)
230 | openTime = fields.IntField(null=True)
231 | endTime = fields.IntField(null=True)
232 | gachaPoolName = fields.CharField(null=True, max_length=255)
233 | gachaPoolSummary = fields.CharField(null=True, max_length=255)
234 | gachaPoolDetail = fields.CharField(null=True, max_length=1024)
235 | guarantee5Avail = fields.IntField(null=True)
236 | guarantee5Count = fields.IntField(null=True)
237 | CDPrimColor = fields.CharField(null=True, max_length=255)
238 | CDSecColor = fields.CharField(null=True, max_length=255)
239 | LMTGSID = fields.CharField(null=True, max_length=255)
240 | gachaRuleType = fields.CharField(null=True, max_length=255)
241 |
242 | storeTextColor = fields.CharField(null=True, max_length=255)
243 |
244 | linkageRuleId = fields.CharField(null=True, max_length=255)
245 | linkageParam = fields.JSONField(null=True)
246 |
247 | dynMeta = fields.JSONField(null=True)
248 |
249 | class Meta:
250 | table = "gacha_pool"
251 |
252 |
253 | class SkinModel(Model):
254 | """皮肤"""
255 | skinId = fields.CharField(null=True, max_length=255, description="皮肤代码")
256 | charId = fields.CharField(null=True, max_length=255, description="干员代码")
257 | tokenSkinMap = fields.JSONField(null=True)
258 | illustId = fields.CharField(null=True, max_length=255)
259 | dynIllustId = fields.CharField(null=True, max_length=255)
260 | avatarId = fields.CharField(null=True, max_length=255)
261 | portraitId = fields.CharField(null=True, max_length=255)
262 | dynPortraitId = fields.CharField(null=True, max_length=255)
263 | dynEntranceId = fields.CharField(null=True, max_length=255)
264 | buildingId = fields.CharField(null=True, max_length=255)
265 | battleSkin = fields.JSONField(null=True)
266 | isBuySkin = fields.BooleanField(null=True)
267 | tmplId = fields.CharField(null=True, max_length=255)
268 | voiceId = fields.CharField(null=True, max_length=255)
269 | voiceType = fields.CharField(null=True, max_length=255)
270 | displaySkin = fields.JSONField(null=True)
271 |
272 | class Meta:
273 | table = "skin"
274 |
275 |
276 | class StageModel(Model):
277 | """关卡"""
278 | stageType = fields.CharField(null=True, max_length=255)
279 | difficulty = fields.CharField(null=True, max_length=255)
280 | performanceStageFlag = fields.CharField(null=True, max_length=255)
281 | diffGroup = fields.CharField(null=True, max_length=255)
282 | unlockCondition = fields.JSONField(null=True)
283 | stageId = fields.CharField(null=True, max_length=255)
284 | levelId = fields.CharField(null=True, max_length=255)
285 | zoneId = fields.CharField(null=True, max_length=255)
286 | code = fields.CharField(null=True, max_length=255)
287 | name = fields.CharField(null=True, max_length=255)
288 | description = fields.CharField(null=True, max_length=255)
289 | hardStagedId = fields.CharField(null=True, max_length=255)
290 | dangerLevel = fields.CharField(null=True, max_length=255)
291 | dangerPoint = fields.FloatField(null=True)
292 | loadingPicId = fields.CharField(null=True, max_length=255)
293 | canPractice = fields.BooleanField(null=True)
294 | canBattleReplay = fields.BooleanField(null=True)
295 | apCost = fields.IntField(null=True)
296 | apFailReturn = fields.IntField(null=True)
297 | etItemId = fields.CharField(null=True, max_length=255)
298 | etCost = fields.IntField(null=True)
299 | etFailReturn = fields.IntField(null=True)
300 | etButtonStyle = fields.CharField(null=True, max_length=255)
301 | apProtectTimes = fields.IntField(null=True)
302 | diamondOnceDrop = fields.IntField(null=True)
303 | practiceTicketCost = fields.IntField(null=True)
304 | dailyStageDifficulty = fields.IntField(null=True)
305 | expGain = fields.IntField(null=True)
306 | goldGain = fields.IntField(null=True)
307 | loseExpGain = fields.IntField(null=True)
308 | loseGoldGain = fields.IntField(null=True)
309 | passFavor = fields.IntField(null=True)
310 | completeFavor = fields.IntField(null=True)
311 | slProgress = fields.IntField(null=True)
312 | displayMainItem = fields.CharField(null=True, max_length=255)
313 | hilightMark = fields.BooleanField(null=True)
314 | bossMark = fields.BooleanField(null=True)
315 | isPredefined = fields.BooleanField(null=True)
316 | isHardPredefined = fields.BooleanField(null=True)
317 | isSkillSelectablePredefined = fields.BooleanField(null=True)
318 | isStoryOnly = fields.BooleanField(null=True)
319 | appearanceStyle = fields.IntField(null=True)
320 | stageDropInfo = fields.JSONField(null=True)
321 | startButtonOverrideId = fields.CharField(null=True, max_length=255)
322 | isStagePatch = fields.BooleanField(null=True)
323 | mainStageId = fields.CharField(null=True, max_length=255)
324 |
325 | extra_can_use = fields.BooleanField(null=True)
326 |
327 | class Meta:
328 | table = "stage"
329 |
330 |
331 | GAME_SQLITE_MODEL_MODULE_NAME = __name__
332 |
333 |
334 | __all__ = [
335 | "BuildingBuffModel",
336 | "CharacterModel",
337 | "ConstanceModel",
338 | "EquipModel",
339 | "HandbookInfoModel",
340 | "HandbookStageModel",
341 | "ItemModel",
342 | "RichTextStyleModel",
343 | "SkillModel",
344 | "TermDescriptionModel",
345 | "WorkshopFormulaModel",
346 | "GachaPoolModel",
347 | "SkinModel",
348 | "StageModel",
349 |
350 | "GAME_SQLITE_MODEL_MODULE_NAME"
351 | ]
352 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/core/database/plugin_sqlite.py:
--------------------------------------------------------------------------------
1 | """非游戏数据,如用户理智恢复提醒的表"""
2 | from tortoise import fields
3 | from tortoise.models import Model
4 |
5 |
6 | class UserSanityModel(Model):
7 | """理智提醒"""
8 | gid = fields.IntField(null=True)
9 | uid = fields.IntField(null=True)
10 | record_san = fields.IntField(null=True, default=0)
11 | notify_san = fields.IntField(null=True, default=135)
12 | record_time = fields.DatetimeField(null=True)
13 | notify_time = fields.DatetimeField(null=True)
14 | status = fields.BooleanField(null=True, default=False)
15 |
16 | class Meta:
17 | table = "uo_user_sanity" # uo = UnOfficial
18 |
19 |
20 | class RSSNewsModel(Model):
21 | """游戏公告"""
22 | time = fields.DatetimeField(null=True)
23 | title = fields.CharField(null=True, max_length=255)
24 | content = fields.TextField(null=True)
25 | link = fields.CharField(null=True, max_length=255)
26 |
27 | class Meta:
28 | table = "uo_rss_news" # uo = UnOfficial
29 |
30 |
31 | class MAACopilotSubsModel(Model):
32 | """抄作业"""
33 | sub_groups = fields.CharField(max_length=255) # 哪些群
34 | sub_keyword = fields.CharField(max_length=255, null=True) # 什么关键词
35 | latest_upload_time = fields.BigIntField(default=0) # 最新上传时间
36 |
37 | latest_id = fields.BigIntField(default=0) # 作业编号
38 | operators = fields.JSONField(null=True) # 阵容(干员、技能)
39 | stage = fields.CharField(max_length=255, null=True) # 关卡
40 | title = fields.CharField(max_length=255, null=True) # 作业标题
41 | details = fields.CharField(max_length=255, null=True) # 简介
42 |
43 | class Meta:
44 | table = "uo_maa_copilot_subs" # uo = UnOfficial
45 |
46 |
47 | PLUGIN_SQLITE_MODEL_MODULE_NAME = __name__
48 |
49 |
50 | __all__ = [
51 | "UserSanityModel",
52 | "RSSNewsModel",
53 | "MAACopilotSubsModel",
54 |
55 | "PLUGIN_SQLITE_MODEL_MODULE_NAME"
56 | ]
57 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | """异常基类"""
2 | from typing import Union
3 |
4 |
5 | class ArkBaseException(Exception):
6 | """基础异常类"""
7 | def __init__(self, msg: str = "出现了错误,但是未说明具体原因。", details: Union[str, int] = ""):
8 | super().__init__(msg)
9 | self.msg = f"{msg} - {details}"
10 |
11 | def __str__(self):
12 | return self.__repr__()
13 |
14 | def __repr__(self):
15 | return self.msg
16 |
17 |
18 | class NamedCharacterNotExistException(ArkBaseException):
19 | """这个名字的干员不存在"""
20 | def __init__(self, msg: str = "干员不存在!", details: str = ""):
21 | super().__init__(msg, details)
22 |
23 |
24 | class NamedPoolNotExistException(ArkBaseException):
25 | """这个名字的池子不存在"""
26 | def __init__(self, msg: str = "卡池不存在!", details: str = ""):
27 | super().__init__(msg, details)
28 |
29 |
30 | class MAAFailedResponseException(ArkBaseException):
31 | """响应错误"""
32 | def __init__(self, msg: str = "作业站响应错误!", details: str = ""):
33 | super().__init__(msg, details)
34 |
35 |
36 | class MAANoResultException(ArkBaseException):
37 | """没有作业"""
38 | def __init__(self, msg: str = "没有查询到结果!", details: str = ""):
39 | super().__init__(msg, details)
40 |
41 |
42 | class NoHandbookInfoException(ArkBaseException):
43 | """没有档案"""
44 | def __init__(self, msg: str = "干员没有档案!", details: str = ""):
45 | super().__init__(msg, details)
46 |
47 |
48 | __all__ = [
49 | "NamedCharacterNotExistException",
50 | "NamedPoolNotExistException",
51 | "NoHandbookInfoException",
52 |
53 | "MAAFailedResponseException",
54 | "MAANoResultException"
55 | ]
56 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/game_draw_card/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 抽卡
3 | 1. 获取卡池信息 (PRTS)
4 | - 卡池名称
5 | - 卡池封面
6 | - 卡池类型
7 | - 限时寻访
8 | - 非标准寻访
9 | - 春节池 (春节)
10 | - 跨年池 (跨年欢庆)
11 | - 周年池 (庆典)
12 | - 夏活池 (夏季)
13 | - 联动池 (else)
14 | - 标准寻访
15 | - 单up
16 | - 定向寻访
17 | - 联合行动
18 | - 常驻寻访
19 | - 标准寻访
20 | - 中坚寻访
21 | - 新手寻访
22 | - 开放时间
23 | - up干员
24 | - up概率
25 | - 能抽出的干员
26 |
27 | 2. 获取干员信息 (GITHUB)
28 | - gamedata
29 | - gameimage
30 |
31 | 3. 按概率随机
32 | """
33 |
34 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/game_guess_operator/__init__.py:
--------------------------------------------------------------------------------
1 | """猜干员,改自 wordle"""
2 | from nonebot import on_shell_command, on_command, on_message
3 | from nonebot.plugin import PluginMetadata
4 | from nonebot.params import ShellCommandArgv, CommandArg, EventPlainText
5 | from nonebot.exception import ParserExit
6 | from nonebot.rule import Rule, ArgumentParser, to_me
7 | from nonebot.matcher import Matcher
8 | from nonebot.typing import T_State
9 | from nonebot.adapters.onebot.v11 import Message, MessageEvent, MessageSegment, GroupMessageEvent
10 |
11 | import asyncio
12 | import shlex
13 | from typing import List, Dict, Optional
14 | from io import BytesIO
15 | from dataclasses import dataclass
16 | from asyncio import TimerHandle
17 |
18 |
19 | from .data_source import *
20 | from ..core.models_v3 import Character
21 | from ..utils.general import nickname_swap
22 |
23 |
24 | GAMES: Dict[str, GuessCharacter] = {} # 记录游戏数据
25 | TIMERS: Dict[str, TimerHandle] = {} # 计时
26 |
27 |
28 | @dataclass
29 | class Options:
30 | hint: bool = False
31 | stop: bool = False
32 | cht_name: str = None
33 |
34 |
35 | parser = ArgumentParser("arkguess", description="猜干员")
36 | parser.add_argument("--hint", action="store_true", help="提示")
37 | parser.add_argument("--stop", action="store_true", help="结束游戏")
38 | parser.add_argument("cht_name", nargs="?", help="干员名")
39 |
40 |
41 | arkguess = on_shell_command("猜干员", parser=parser)
42 | @arkguess.handle()
43 | async def _(matcher: Matcher, event: MessageEvent, argv: List[str] = ShellCommandArgv()):
44 | """开始游戏"""
45 | await handle_arkguess(matcher, event, argv)
46 |
47 |
48 | async def handle_arkguess(matcher: Matcher, event: MessageEvent, argv: List[str]):
49 | async def send(message: Optional[str] = None, image: Optional[BytesIO] = None):
50 | if not (message or image):
51 | await matcher.finish()
52 | msg_ = Message()
53 | if image:
54 | msg_.append(MessageSegment.image(image))
55 | if message:
56 | msg_.append(message)
57 | await matcher.finish(msg_)
58 |
59 | try:
60 | args = parser.parse_args(argv)
61 | except ParserExit as e:
62 | if e.status == 0:
63 | await send("小笨蛋,命令输错了哦!")
64 | await send()
65 |
66 | options = Options(**vars(args))
67 | cid = f"group_{event.group_id}" if isinstance(event, GroupMessageEvent) else f"group_{event.user_id}"
68 | if not GAMES.get(cid):
69 | if options.cht_name or options.stop or options.hint:
70 | await matcher.finish("小笨蛋,没有正在进行的游戏哦!")
71 |
72 | character = await get_random_character()
73 | game = GuessCharacter(cht=character)
74 | GAMES[cid] = game
75 | set_timeout(matcher, cid)
76 |
77 | await send(f"你有{game.times}次机会猜出干员,请发送“#干员名”猜干员,如“#艾雅法拉”", await game.draw())
78 |
79 | # 手动结束游戏
80 | if options.stop:
81 | game = GAMES.pop(cid)
82 | msg = "游戏已结束"
83 | if game.guessed:
84 | msg += f"\n{await game.get_result()}"
85 | await send(msg)
86 |
87 | game = GAMES[cid]
88 | set_timeout(matcher, cid)
89 |
90 | # 提示
91 | if options.hint:
92 | await send(message=await game.get_hint())
93 |
94 | options.cht_name = await nickname_swap(options.cht_name)
95 | cht = await Character.parse_name(options.cht_name)
96 | result = await game.guess(cht)
97 | if result in [GuessResult.WIN, GuessResult.LOSE]:
98 | GAMES.pop(cid)
99 | await send(
100 | (
101 | "恭喜你猜出了干员!"
102 | if result == GuessResult.WIN
103 | else "很遗憾,没有人猜出来呢"
104 | ) + f"\n{await game.get_result()}",
105 | await game.draw()
106 | )
107 |
108 | elif result == GuessResult.DUPLICATE:
109 | await send("小笨蛋,已经猜过这个干员了哦")
110 | elif result == GuessResult.ILLEGAL:
111 | await send(f"你确定 {cht.name} 是咱干员吗?")
112 | else:
113 | await send(image=await game.draw())
114 |
115 |
116 | def set_timeout(matcher: Matcher, cid: str, timeout: float = 300):
117 | """设置游戏超时,默认5分钟"""
118 | timer = TIMERS.get(cid)
119 | if timer:
120 | timer.cancel()
121 | loop = asyncio.get_running_loop()
122 | timer = loop.call_later(
123 | timeout, lambda: asyncio.ensure_future(stop_game(matcher, cid))
124 | )
125 | TIMERS[cid] = timer
126 |
127 |
128 | async def stop_game(matcher: Matcher, cid: str):
129 | """超时自动停止"""
130 | TIMERS.pop(cid)
131 | if GAMES.get(cid):
132 | game = GAMES.pop(cid)
133 | msg = "猜干员超时,游戏结束"
134 | if game.guessed:
135 | msg += f"\n{await game.get_result()}"
136 | await matcher.finish(msg)
137 |
138 |
139 | def shortcut(cmd: str, argv: List[str] = None, **kwargs):
140 | if not argv:
141 | argv = []
142 | command = on_command(cmd, **kwargs)
143 |
144 | @command.handle()
145 | async def _(matcher: Matcher, event: MessageEvent, msg: Message = CommandArg()):
146 | try:
147 | args = shlex.split(msg.extract_plain_text().strip())
148 | except Exception as e:
149 | args = []
150 | await handle_arkguess(matcher, event, argv + args)
151 |
152 |
153 | def is_game_running(event: MessageEvent) -> bool:
154 | """判断游戏运行"""
155 | return bool(GAMES.get(f"group_{event.group_id}")) if isinstance(event, GroupMessageEvent) else bool(GAMES.get(f"group_{event.user_id}"))
156 |
157 |
158 | def get_word_input(state: T_State, msg: str = EventPlainText()) -> bool:
159 | """获取输入干员"""
160 | if msg.startswith("#"):
161 | state["cht_name"] = msg[1:]
162 | return True
163 | return False
164 |
165 |
166 | shortcut("猜干员", [], rule=to_me())
167 | shortcut("提示", ["--hint"], rule=is_game_running)
168 | shortcut("结束", ["--stop"], rule=is_game_running)
169 |
170 |
171 | word_matcher = on_message(Rule(is_game_running) & get_word_input)
172 | @word_matcher.handle()
173 | async def _(matcher: Matcher, event: MessageEvent, state: T_State):
174 | cht_name: str = state["cht_name"]
175 | await handle_arkguess(matcher, event, [cht_name])
176 |
177 |
178 | __plugin_meta__ = PluginMetadata(
179 | name="猜干员",
180 | description="与wordle玩法相同,猜明日方舟干员",
181 | usage=(
182 | "命令:"
183 | "\n 猜干员 => 开始新游戏"
184 | "\n #干员名称(例: #艾雅法拉) => 猜干员"
185 | "\n 提示 => 查看答案干员的信息"
186 | "\n 结束 => 结束当前游戏"
187 | ),
188 | extra={
189 | "name": "guess_operator",
190 | "author": "NumberSir",
191 | "version": "0.1.0"
192 | }
193 | )
194 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/game_guess_operator/data_source.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from ..core.models_v3 import Character
4 | from ..configs.path_config import PathConfig
5 |
6 | from nonebot import get_driver
7 |
8 | from typing import List, Set
9 | from enum import Enum
10 | from PIL import Image, ImageFont
11 | from PIL.ImageDraw import Draw
12 | from io import BytesIO
13 |
14 |
15 | driver = get_driver()
16 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
17 | data_path = Path(pcfg.arknights_data_path).absolute()
18 | font_path = Path(pcfg.arknights_font_path).absolute()
19 | GUESS_IMG_PATH = data_path / "guess_character"
20 |
21 |
22 | async def get_all_characters() -> List["Character"]:
23 | """所有干员"""
24 | return await Character.all()
25 |
26 |
27 | async def get_random_character() -> "Character":
28 | """随机一个干员"""
29 | return await Character.random()
30 |
31 |
32 | class GuessResult(Enum):
33 | WIN = 0 # 猜对了
34 | LOSE = 1 # 没猜出
35 | DUPLICATE = 2 # 猜过了
36 | ILLEGAL = 3 # 不是干员
37 |
38 |
39 | class GuessCharacter:
40 | def __init__(self, cht: "Character"):
41 | self._answer: "Character" = cht # 目标
42 | self._times: int = 8 # 能猜的次数
43 | self._guessed: List["Character"] = [] # 猜过的
44 |
45 | self._block_size = (40, 40) # 表情块尺寸
46 | self._block_padding = (10, 10) # 表情块之间间距
47 | self._padding = (20, 20) # 边界间距
48 | self._font_size = 32 # 字体大小
49 | self._font = ImageFont.truetype((font_path / "Arknights-zh.otf").__str__(), self._font_size)
50 | self._bg_color = (255, 255, 255) # 背景颜色
51 |
52 | self._correct_face = Image.open(GUESS_IMG_PATH / "correct.png", "r").convert("RGBA") # 完全一致
53 | self._vague_face = Image.open(GUESS_IMG_PATH / "vague.png", "r").convert("RGBA") # 职业 / 阵营部分一致 / 答案高台地面均可放,猜高台或地面
54 | self._wrong_face = Image.open(GUESS_IMG_PATH / "wrong.png", "r").convert("RGBA") # 完全不同
55 | self._up_face = Image.open(GUESS_IMG_PATH / "up.png", "r").convert("RGBA") # 低于目标星级
56 | self._down_face = Image.open(GUESS_IMG_PATH / "down.png", "r").convert("RGBA") # 高于目标星级
57 |
58 | async def guess(self, cht: Character) -> GuessResult:
59 | """每次猜完"""
60 | if not await self.is_character_legal(cht.name):
61 | return GuessResult.ILLEGAL
62 | if cht.name in {_.name for _ in self._guessed}:
63 | return GuessResult.DUPLICATE
64 | self._guessed.append(cht)
65 |
66 | if cht.name == self._answer.name:
67 | return GuessResult.WIN
68 | if len(self._guessed) == self._times:
69 | return GuessResult.LOSE
70 |
71 | async def draw_bar(self, cht: Character) -> Image:
72 | """画一行"""
73 | avatar = cht.avatar.resize((80, 80))
74 | rarity_face = self.get_rarity_face(cht)
75 | profession_face = self.get_profession_face(cht)
76 | faction_face = self.get_faction_face(cht)
77 | race_face = await self.get_race_face(cht)
78 | position_face = self.get_position_face(cht)
79 |
80 | bar = Image.new("RGBA", size=(80*6+16*5, 80), color=(0, 0, 0, 0))
81 | faces = [avatar, rarity_face, profession_face, faction_face, race_face, position_face]
82 | for idx, face in enumerate(faces):
83 | bar.paste(face, box=((80+16)*idx, 0), mask=face.split()[3])
84 | return bar
85 |
86 | async def draw(self) -> BytesIO:
87 | """正式绘画逻辑"""
88 | main_bg = Image.new("RGBA", size=(80*6+16*5+16*2, 32+80*8+16*8+16*2), color=(255, 255, 255, 255))
89 |
90 | # 先画头
91 | header = Image.new(mode="RGBA", size=(80*6+16*5, 32), color=(0, 0, 0, 0))
92 | draw = Draw(header)
93 | texts = ["干员", "星级", "职业", "阵营", "种族", "站位"]
94 | for i, t in enumerate(texts):
95 | draw.text(xy=((80+16)*i+40, 16), text=t, font=self._font, anchor="mm", fill=(0, 0, 0))
96 | main_bg.paste(im=header, box=(16, 16), mask=header.split()[3])
97 |
98 | # 一行一行画
99 | for idx, cht in enumerate(self._guessed):
100 | bar = await self.draw_bar(cht)
101 | main_bg.paste(im=bar, box=(16, 16+32+16+(80+16)*idx), mask=bar.split()[3])
102 |
103 | return self.save(main_bg)
104 |
105 | @staticmethod
106 | def save(image: Image) -> BytesIO:
107 | """临时缓存"""
108 | output = BytesIO()
109 | image = image.convert("RGB")
110 | image.save(output, format="jpeg")
111 | return output
112 |
113 | @staticmethod
114 | async def is_character_legal(cht_name: str) -> bool:
115 | """判断是不是咱的干员"""
116 | return cht_name in {_.name for _ in await get_all_characters()}
117 |
118 | def get_rarity_face(self, cht: Character) -> Image:
119 | """星级检查"""
120 | if cht.rarity > self._answer.rarity:
121 | return self._down_face
122 | elif cht.rarity < self._answer.rarity:
123 | return self._up_face
124 | else:
125 | return self._correct_face
126 |
127 | def get_profession_face(self, cht: Character) -> Image:
128 | """职业检查"""
129 | if cht.sub_profession_id == self._answer.sub_profession_id:
130 | return self._correct_face
131 | elif cht.profession_id == self._answer.profession_id:
132 | return self._vague_face
133 | else:
134 | return self._wrong_face
135 |
136 | def get_faction_face(self, cht: Character) -> Image:
137 | """阵营检查"""
138 | if cht.faction_id == self._answer.faction_id:
139 | return self._correct_face
140 | elif cht.faction_id.startswith(self._answer.faction_id) or self._answer.faction_id.startswith(cht.faction_id):
141 | return self._vague_face
142 | else:
143 | return self._wrong_face
144 |
145 | async def get_race_face(self, cht: Character) -> Image:
146 | """种族检查"""
147 | return self._correct_face if await cht.get_race() == await self._answer.get_race() else self._wrong_face
148 |
149 | def get_position_face(self, cht: Character) -> Image:
150 | """站位检查"""
151 | if cht.position == self._answer.position:
152 | return self._correct_face
153 | elif self._answer.position == "BOTH":
154 | return self._vague_face
155 | else:
156 | return self._wrong_face
157 |
158 | async def get_hint(self) -> str:
159 | """返回提示"""
160 | return (
161 | f"星数:{'★'*(self._answer.rarity+1)}"
162 | f"\n职业:{await self._answer.get_profession_name()}-{await self._answer.get_sub_profession_name()}"
163 | f"\n种族:{await self._answer.get_race()}"
164 | f"\n性别:{await self._answer.get_sex()}"
165 | f"\n阵营:{await self._answer.get_faction_name()}"
166 | f"\n站位:{self._answer.position}"
167 | )
168 |
169 | async def get_result(self) -> str:
170 | """返回结果"""
171 | return (
172 | f"答案: {self._answer.name}"
173 | f"\n{await self.get_hint()}"
174 | )
175 |
176 | @property
177 | def times(self) -> int:
178 | """最多猜几次"""
179 | return self._times
180 |
181 | @property
182 | def guessed(self) -> List["Character"]:
183 | """猜过的干员"""
184 | return self._guessed
185 |
186 |
187 | __all__ = [
188 | "get_all_characters",
189 | "get_random_character",
190 | "GuessResult",
191 | "GuessCharacter"
192 | ]
193 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/help.py:
--------------------------------------------------------------------------------
1 | """帮助"""
2 | from .game_guess_operator import __plugin_meta__ as GUESS_META
3 | from .misc_operator_birthday import __plugin_meta__ as BIRTHDAY_META
4 | from .misc_monster_siren import __plugin_meta__ as SIREN_META
5 | from .tool_announce_push import __plugin_meta__ as ANNOUNCE_META
6 | from .tool_fetch_maa_copilot import __plugin_meta__ as MAA_META
7 | from .tool_operator_info import __plugin_meta__ as INFO_META
8 | from .tool_open_recruitment import __plugin_meta__ as RECRUIT_META
9 | from .tool_sanity_notify import __plugin_meta__ as SAN_META
10 | from .utils import __plugin_meta__ as UTILS_META
11 |
12 | from nonebot import on_command
13 | from nonebot.adapters.onebot.v11 import MessageSegment
14 | from io import BytesIO
15 | from nonebot_plugin_imageutils import text2image
16 |
17 |
18 | HELP_DATAS = [
19 | GUESS_META,
20 | BIRTHDAY_META,
21 | SIREN_META,
22 | ANNOUNCE_META,
23 | MAA_META,
24 | INFO_META,
25 | RECRUIT_META,
26 | SAN_META,
27 | UTILS_META
28 | ]
29 |
30 |
31 | help_msg = on_command("方舟帮助", aliases={"arkhelp"})
32 |
33 | @help_msg.handle()
34 | async def _():
35 | result = "\n".join(
36 | f"[color=red]{data.name}[/color]"
37 | f"\n{data.description}"
38 | f"\n{data.usage}\n"
39 | for data in HELP_DATAS
40 | )
41 | output = BytesIO()
42 | text2image(result).save(output, "png")
43 | await help_msg.finish(MessageSegment.image(output))
44 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/misc_monster_siren/__init__.py:
--------------------------------------------------------------------------------
1 | """即点歌"""
2 | from nonebot import on_command
3 | from nonebot.plugin import PluginMetadata
4 | from nonebot.params import CommandArg, Arg
5 | from nonebot.matcher import Matcher
6 | from nonebot.adapters.onebot.v11 import Message
7 |
8 | from .data_source import search_cloud, search_tencent
9 |
10 |
11 | siren = on_command("塞壬点歌")
12 |
13 |
14 | @siren.handle()
15 | async def _(matcher: Matcher, args: Message = CommandArg()):
16 | args = args.extract_plain_text().strip()
17 | if args:
18 | matcher.set_arg("keywords", args)
19 |
20 |
21 | @siren.got(key="keywords", prompt="请发送要点的歌名:")
22 | async def _(keywords: str = Arg()):
23 | await siren.send("搜索中...")
24 | await siren.finish(
25 | await search_cloud(keywords)
26 | )
27 |
28 |
29 | __plugin_meta__ = PluginMetadata(
30 | name="塞壬点歌",
31 | description="即网易云点歌",
32 | usage=(
33 | "命令:"
34 | "\n 塞壬点歌 [歌曲名] => 点歌,以卡片形式发到群内"
35 | ),
36 | extra={
37 | "name": "monster_siren",
38 | "author": "NumberSir",
39 | "version": "0.1.0"
40 | }
41 | )
42 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/misc_monster_siren/data_source.py:
--------------------------------------------------------------------------------
1 | import httpx
2 | from typing import Union
3 | from nonebot.adapters.onebot.v11 import MessageSegment
4 |
5 |
6 | async def search_cloud(keyword: str) -> Union[str, MessageSegment]:
7 | """网易云"""
8 | url = "https://music.163.com/api/cloudsearch/pc"
9 | params = {"s": keyword, "type": 1, "offset": 0, "limit": 1}
10 | async with httpx.AsyncClient() as client:
11 | resp = await client.post(url, params=params)
12 | result = resp.json()
13 | if songs := result["result"]["songs"]:
14 | return MessageSegment.music("163", songs[0]["id"])
15 | return f"网易云音乐中没有找到“{keyword}”相关的歌曲哦"
16 |
17 |
18 | async def search_tencent(keyword: str) -> Union[str, MessageSegment]:
19 | """qq音乐"""
20 | url = "https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg"
21 | params = {
22 | "format": "json",
23 | "inCharset": "utf-8",
24 | "outCharset": "utf-8",
25 | "notice": 0,
26 | "platform": "yqq.json",
27 | "needNewCode": 0,
28 | "uin": 0,
29 | "hostUin": 0,
30 | "is_xml": 0,
31 | "key": keyword,
32 | }
33 | async with httpx.AsyncClient() as client:
34 | resp = await client.get(url, params=params)
35 | result = resp.json()
36 | if songs := result["data"]["song"]["itemlist"]:
37 | return MessageSegment.music("qq", songs[0]["id"])
38 | return f"QQ音乐中没有找到“{keyword}”相关的歌曲哦"
39 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/misc_operator_birthday/__init__.py:
--------------------------------------------------------------------------------
1 | """干员生日提醒"""
2 | from pathlib import Path
3 |
4 | from nonebot import on_command, get_driver, logger
5 | from nonebot.plugin import PluginMetadata
6 | from nonebot.adapters.onebot.v11 import MessageSegment, Message
7 | from typing import List
8 | from datetime import datetime
9 | from PIL import Image, ImageFont
10 | from PIL.ImageDraw import Draw
11 | from io import BytesIO
12 |
13 | from ..core.models_v3 import Character
14 | from ..exceptions import NoHandbookInfoException
15 | from ..utils import text_border
16 | from ..configs.path_config import PathConfig
17 |
18 |
19 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
20 | font_path = Path(pcfg.arknights_font_path).absolute()
21 |
22 |
23 | today_birthday = on_command("今日干员")
24 |
25 |
26 | @today_birthday.handle()
27 | async def _():
28 | today = datetime.now().strftime("%m月%d日").strip("0").replace("月0", "月")
29 | characters = await Character.all()
30 | try:
31 | results: List["Character"] = [
32 | cht
33 | for cht in characters
34 | if (await cht.get_handbook_info()).story_text_audio.birthday == today
35 | ]
36 | except NoHandbookInfoException as e:
37 | await today_birthday.finish(f"嗯唔……干员的档案数据不全哦")
38 |
39 | if not results:
40 | await today_birthday.finish("哦呀?今天没有干员过生日哦……")
41 | try:
42 | main_background = Image.new("RGBA", (24*2+128*len(results)+16*(len(results)-1), 24*2+128+24), (0, 0, 0, 0))
43 | for idx, cht in enumerate(results):
44 | cht_bg = Image.new("RGBA", (128, 128+24), (150, 150, 150, 150))
45 | icon = cht.avatar.convert("RGBA").resize((128, 128))
46 | cht_bg.paste(im=icon, box=(0, 0), mask=icon.split()[3])
47 | text_border(
48 | cht.name,
49 | Draw(cht_bg),
50 | x=64,
51 | y=(128 + 12 + 152 * (idx // 6)),
52 | anchor="mm",
53 | font=ImageFont.truetype((font_path / "Arknights-zh.otf").__str__(), 20),
54 | fill_colour=(255, 255, 255, 255),
55 | shadow_colour=(0, 0, 0, 255)
56 | )
57 | main_background.paste(cht_bg, (24+idx*(128+16), 24), mask=cht_bg.split()[3])
58 | except FileNotFoundError as e:
59 | logger.error("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
60 | await today_birthday.finish("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
61 |
62 | output = BytesIO()
63 | main_background.save(output, format="png")
64 | await today_birthday.finish(
65 | Message(
66 | MessageSegment.image(output) +
67 | "\n今天过生日的干员有这些哦"
68 | )
69 | )
70 |
71 |
72 | __plugin_meta__ = PluginMetadata(
73 | name="今日干员",
74 | description="查看今日过生日的干员",
75 | usage=(
76 | "命令:"
77 | "\n 今日干员 => 查看今日过生日的干员"
78 | ),
79 | extra={
80 | "name": "operator_birthday",
81 | "author": "NumberSir",
82 | "version": "0.1.0"
83 | }
84 | )
85 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_announce_push/__init__.py:
--------------------------------------------------------------------------------
1 | """游戏公告推送"""
2 | from pathlib import Path
3 |
4 | from aiofiles import open as aopen
5 | from nonebot import logger, get_bot, on_command, get_driver
6 | from nonebot.adapters.onebot.v11 import Bot, MessageSegment, Message
7 | from nonebot.exception import ActionFailed
8 | from nonebot.params import CommandArg
9 | from nonebot.plugin import PluginMetadata
10 | from nonebot_plugin_apscheduler import scheduler
11 | from nonebot_plugin_htmlrender import html_to_pic
12 |
13 | from .data_source import get_news
14 | from ..configs.scheduler_config import SchedulerConfig
15 | from ..core.database import RSSNewsModel
16 |
17 | scfg = SchedulerConfig.parse_obj(get_driver().config.dict())
18 |
19 | latest_news = on_command("方舟最新公告")
20 | add_group = on_command("添加方舟推送群", aliases={"ADDGROUP"})
21 | del_group = on_command("删除方舟推送群", aliases={"DELGROUP"})
22 | get_group = on_command("查看方舟推送群", aliases={"GETGROUP"})
23 |
24 |
25 | @latest_news.handle()
26 | async def _():
27 | news = await RSSNewsModel.all().order_by("time").first()
28 | await latest_news.send("获取最新公告中 ...")
29 | if not news:
30 | try:
31 | news_list = await get_news()
32 | except:
33 | logger.error("获取最新公告出错")
34 | else:
35 | news = news_list[0]
36 |
37 | image = await html_to_pic(
38 | html=news.content
39 | )
40 | try:
41 | await latest_news.finish(
42 | Message(
43 | MessageSegment.image(image)
44 | + "舟舟发布了一条新公告"
45 | f"\n发布时间: {news.time.__str__()[:10]}"
46 | f"\n{news.link}"
47 | )
48 | )
49 | except ActionFailed as e:
50 | await latest_news.finish(
51 | "公告截图失败..."
52 | f"\n发布时间: {news.time.__str__()[:10]}"
53 | f"\n{news.link}"
54 | )
55 |
56 |
57 | @add_group.handle()
58 | async def _(arg: Message = CommandArg()):
59 | args = arg.extract_plain_text().strip().split()
60 | if not args or not all((_.isnumeric() for _ in args)):
61 | await add_group.finish()
62 |
63 | if not (Path(__file__).parent / "groups.txt").exists():
64 | async with aopen(Path(__file__).parent / "groups.txt", "w") as fp:
65 | await fp.write(f"{' '.join(args)}")
66 | await add_group.finish("添加成功!", at_sender=True)
67 |
68 | async with aopen(Path(__file__).parent / "groups.txt", "r") as fp:
69 | local_groups = await fp.read()
70 | async with aopen(Path(__file__).parent / "groups.txt", "w") as fp:
71 | await fp.write(" ".join(list(set(local_groups.split() + args))))
72 | await add_group.finish("添加成功!", at_sender=True)
73 |
74 |
75 | @del_group.handle()
76 | async def _(arg: Message = CommandArg()):
77 | args = arg.extract_plain_text().strip().split()
78 | if not args or not all((_.isnumeric() for _ in args)):
79 | await del_group.finish()
80 |
81 | if not (Path(__file__).parent / "groups.txt").exists():
82 | async with aopen(Path(__file__).parent / "groups.txt", "w") as fp:
83 | pass
84 | await del_group.finish("删除成功!", at_sender=True)
85 |
86 | async with aopen(Path(__file__).parent / "groups.txt", "r") as fp:
87 | local_groups = await fp.read()
88 | async with aopen(Path(__file__).parent / "groups.txt", "w") as fp:
89 | groups = {
90 | _ for _ in local_groups.split()
91 | if _ not in args
92 | }
93 | await fp.write(f"{' '.join(list(groups))}")
94 | await del_group.finish("删除成功!", at_sender=True)
95 |
96 |
97 | @get_group.handle()
98 | async def _():
99 | if not (Path(__file__).parent / "groups.txt").exists():
100 | await get_group.finish("小笨蛋,尚未添加任何推送群哦!", at_sender=True)
101 |
102 | async with aopen(Path(__file__).parent / "groups.txt", "r") as fp:
103 | groups = await fp.read()
104 | if not groups:
105 | await get_group.finish("小笨蛋,尚未添加任何推送群哦!", at_sender=True)
106 |
107 | await get_group.finish(
108 | "当前自动推送最新公告的群聊: "
109 | f"\n{', '.join(groups.split())}"
110 | )
111 |
112 |
113 | @scheduler.scheduled_job(
114 | "interval",
115 | minutes=scfg.announce_push_interval,
116 | )
117 | async def _():
118 | if scfg.announce_push_switch:
119 | logger.info("checking rss news...")
120 | try:
121 | bot: Bot = get_bot()
122 | except ValueError:
123 | pass
124 |
125 | try:
126 | news_list = await get_news()
127 | except: # TODO
128 | logger.error("获取最新公告出错")
129 | else:
130 | if news_list:
131 | if not (Path(__file__).parent / "groups.txt").exists():
132 | async with aopen(Path(__file__).parent / "groups.txt", "w") as fp:
133 | pass
134 | async with aopen(Path(__file__).parent / "groups.txt", "r") as fp:
135 | groups = (await fp.read()).split()
136 | if groups:
137 | for news in news_list:
138 | for group in groups:
139 | image = await html_to_pic(
140 | html=news.content
141 | )
142 | try:
143 | await bot.send_group_msg(
144 | group_id=int(group),
145 | message=Message(
146 | MessageSegment.image(image)
147 | + "舟舟发布了一条新公告"
148 | f"\n发布时间: {news.time.__str__()[:10]}"
149 | f"\n{news.link}"
150 | )
151 | )
152 | except ActionFailed as e:
153 | await bot.send_group_msg(
154 | group_id=int(group),
155 | message=Message(
156 | "公告截图失败..."
157 | f"\n发布时间: {news.time.__str__()[:10]}"
158 | f"\n{news.link}"
159 | )
160 | )
161 |
162 |
163 | __plugin_meta__ = PluginMetadata(
164 | name="公告推送",
165 | description="获取并推送最新的方舟公告/新闻",
166 | usage=(
167 | "命令:"
168 | "\n 方舟最新公告 => 获取最新公告"
169 | "\n 添加方舟推送群 / ADDGROUP => 添加自动推送的群号"
170 | "\n 删除方舟推送群 / DELGROUP => 删除自动推送的群号"
171 | "\n 查看方舟推送群 / GETGROUP => 查看自动推送的群号"
172 | "\n无命令:"
173 | "\n 自动推送方舟最新公告的截图、发布时间、链接"
174 | ),
175 | extra={
176 | "name": "announce_push",
177 | "author": "NumberSir",
178 | "version": "0.1.0"
179 | }
180 | )
181 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_announce_push/data_source.py:
--------------------------------------------------------------------------------
1 | import feedparser
2 | from typing import Optional, List
3 | from nonebot import get_driver
4 | from datetime import datetime
5 |
6 | from ..configs import PathConfig, ProxyConfig
7 | from ..core.database import RSSNewsModel
8 |
9 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
10 | xcfg = ProxyConfig.parse_obj(get_driver().config.dict())
11 |
12 |
13 | async def get_news() -> Optional[List["RSSNewsModel"]]:
14 | """游戏公告/新闻"""
15 | url = f"{xcfg.rss_site}/arknights/news?filterout_title=封禁&limit=3"
16 | rss_data = feedparser.parse(url)
17 | if not rss_data or rss_data["status"] != 200:
18 | raise # TODO
19 | if not rss_data["entries"]:
20 | return None
21 |
22 | latest_news = []
23 | for news in rss_data["entries"]:
24 | link = news["link"]
25 | data = await RSSNewsModel.filter(link=link).first()
26 | if data:
27 | continue
28 |
29 | time = datetime(*news["published_parsed"][:7])
30 | title = news["title"],
31 | # content = get_plain_text(news["summary"])
32 | content = news["summary"]
33 | await RSSNewsModel.create(
34 | time=time, title=title, content=content, link=link
35 | )
36 | latest_news.append(
37 | RSSNewsModel(time=time, title=title, content=content, link=link)
38 | )
39 | return latest_news
40 |
41 |
42 | async def get_bilibili_dynamics():
43 | """B站动态"""
44 | ...
45 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_announce_push/groups.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/src/tool_announce_push/groups.txt
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_fetch_maa_copilot/__init__.py:
--------------------------------------------------------------------------------
1 | """从 https://prts.plus 获取自动作业数据"""
2 | from nonebot import on_command, get_bot, logger, get_driver
3 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, Bot, MessageSegment
4 | from nonebot.params import CommandArg
5 | from nonebot.plugin import PluginMetadata
6 | from nonebot_plugin_apscheduler import scheduler
7 |
8 | from .data_source import (
9 | add_maa_sub,
10 | del_maa_sub,
11 | que_maa_sub,
12 | fetch_works,
13 | SubManager,
14 | process_works,
15 | process_copilot_data,
16 | build_result_image,
17 | DEFAULT_PARAMS,
18 | ORDERS
19 | )
20 | from ..configs.scheduler_config import SchedulerConfig
21 | from ..exceptions import MAAFailedResponseException, MAANoResultException
22 |
23 | query_copilot = on_command("maa查作业", aliases={"maa抄作业"})
24 | add_sub = on_command("maa添加订阅", aliases={"ADDMAA"})
25 | del_sub = on_command("maa删除订阅", aliases={"DELMAA"})
26 | get_sub = on_command("maa查看订阅", aliases={"GETMAA"})
27 |
28 |
29 | scfg = SchedulerConfig.parse_obj(get_driver().config.dict())
30 | sub_manager = SubManager()
31 |
32 |
33 | @query_copilot.handle()
34 | async def _(args: Message = CommandArg()):
35 | keywords = args.extract_plain_text().strip()
36 | if not keywords:
37 | await add_sub.finish()
38 |
39 | keywords = keywords.split("|")
40 | if len(keywords) == 1:
41 | keywords, order_by = keywords[0], ORDERS["最新"]
42 | elif len(keywords) == 2:
43 | keywords, order_by = keywords[0], ORDERS.get(keywords[1], ORDERS["最新"])
44 | else:
45 | await query_copilot.finish("输入格式错误!正确的输入格式为:\nmaa查作业 关键词1 关键词2 ...\nmaa查作业 关键词1 关键词2 ... | 热度/最新/访问")
46 |
47 | keywords = "+".join(sorted(set(keywords.split())))
48 | params = DEFAULT_PARAMS.copy()
49 | params["document"] = keywords
50 | params["order_by"] = order_by
51 | try:
52 | result = await fetch_works(params)
53 | except (MAANoResultException, MAAFailedResponseException) as e:
54 | await query_copilot.finish(e.msg)
55 |
56 | title, details, stage, operators_str = await process_copilot_data(result[0])
57 | img_bytes = await build_result_image(title, details, stage, operators_str)
58 | await query_copilot.finish(
59 | Message(
60 | MessageSegment.image(img_bytes)) +
61 | f"查询结果: \n\n关键词: \n{', '.join(keywords.split('+'))}\n\n作业代码: \nmaa://{result[0]['id']}"
62 | )
63 |
64 |
65 | @add_sub.handle()
66 | async def _(event: GroupMessageEvent, args: Message = CommandArg()):
67 | group_id = str(event.group_id)
68 | keywords = args.extract_plain_text().strip()
69 | if not keywords:
70 | await add_sub.finish()
71 |
72 | keywords = "+".join(sorted(set(keywords.split())))
73 | result = await add_maa_sub(group_id, keywords)
74 | await add_sub.finish(result)
75 |
76 |
77 | @del_sub.handle()
78 | async def _(event: GroupMessageEvent, args: Message = CommandArg()):
79 | group_id = str(event.group_id)
80 | keywords = args.extract_plain_text().strip()
81 | if not keywords:
82 | await del_sub.finish()
83 |
84 | keywords = "+".join(sorted(set(keywords.split())))
85 | result = await del_maa_sub(group_id, keywords)
86 | await del_sub.finish(result)
87 |
88 |
89 | @get_sub.handle()
90 | async def _(event: GroupMessageEvent):
91 | group_id = str(event.group_id)
92 | result = await que_maa_sub(group_id)
93 | await get_sub.finish(result)
94 |
95 |
96 | @scheduler.scheduled_job(
97 | "interval",
98 | minutes=scfg.maa_copilot_interval,
99 | )
100 | async def _():
101 | if scfg.maa_copilot_switch:
102 | global sub_manager
103 | try:
104 | bot: Bot = get_bot()
105 | except ValueError:
106 | pass
107 | else:
108 | try:
109 | await sub_manager.reload_sub_data()
110 | for _ in range(len(sub_manager.data)):
111 | keyword = await sub_manager.random_sub_data()
112 | if keyword:
113 | logger.info(f"MAA 作业查询中: {keyword}")
114 | params = DEFAULT_PARAMS.copy()
115 | params["document"] = keyword
116 | try:
117 | data = await fetch_works(params)
118 | except MAAFailedResponseException as e:
119 | logger.error(f"MAA查询作业出错: {e.msg}")
120 | except MAANoResultException as e:
121 | logger.error(f"MAA查询作业出错: {e.msg}")
122 | else:
123 | result = await process_works(data, keyword)
124 | if result:
125 | image, id_, groups = result
126 | for group in groups.split():
127 | await bot.send_group_msg(
128 | group_id=int(group),
129 | message=Message(MessageSegment.image(image))
130 | + f"有新的MAA作业: \n\n关键词: \n{', '.join(keyword.split('+'))}\n\n作业代码: \nmaa://{id_}"
131 | )
132 | except Exception as e:
133 | logger.error(f"推送 MAA 作业失败!{e}")
134 |
135 |
136 | __plugin_meta__ = PluginMetadata(
137 | name="MAA 抄作业",
138 | description="按关键词订阅 MAA 的作业站作业",
139 | usage=(
140 | "命令:"
141 | "\n maa查作业 [关键词1 关键词2 ...] => 按关键词组合查作业,默认为最新发布的第一个作业"
142 | "\n maa查作业 [关键词1 关键词2 ...] | [热度/最新/访问] => 同上,不过可以指定按什么顺序查询"
143 | "\n maa添加订阅 [关键词1 关键词2 ...] => 按关键词组合订阅作业"
144 | "\n maa删除订阅 [关键词1 关键词2 ...] => 删除本群对这些关键词组合的订阅"
145 | "\n maa查看订阅 => 查看本群订阅的所有关键词组合"
146 | ),
147 | extra={
148 | "name": "fetch_maa_copilot",
149 | "author": "NumberSir",
150 | "version": "0.1.0"
151 | }
152 | )
153 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_fetch_maa_copilot/data_source.py:
--------------------------------------------------------------------------------
1 | import json
2 | import random
3 | from io import BytesIO
4 | from datetime import datetime
5 | from typing import List, Dict, Tuple, Optional
6 |
7 | import httpx
8 | from nonebot import logger
9 | from nonebot_plugin_imageutils import text2image
10 |
11 | from ..core.database.plugin_sqlite import MAACopilotSubsModel
12 | from ..exceptions import MAAFailedResponseException, MAANoResultException
13 | from ..utils import stage_swap, handbook_stage_swap
14 |
15 | ORDERS = {
16 | "热度": "hot",
17 | "最新": "id",
18 | "访问": "views"
19 | }
20 |
21 | DEFAULT_PARAMS = {
22 | "desc": True,
23 | "limit": 50,
24 | "page": 1,
25 | "order_by": ORDERS["最新"],
26 | "document": "", # 标题、描述、神秘代码
27 | "level_keyword": "", # 关卡名、类型、编号
28 | "operator": "" # 包含、排除干员
29 | }
30 |
31 | POSSIBLE_KEYS = {
32 | "document",
33 | "level_keyword",
34 | "operator"
35 | }
36 |
37 |
38 | async def fetch_works(params: dict) -> List[Dict]:
39 | """根据参数获取结果"""
40 | url = "https://prts.maa.plus/copilot/query"
41 | async with httpx.AsyncClient() as client:
42 | response = await client.get(url, params=params)
43 |
44 | logger.info(response.url)
45 | data = response.json()
46 | if data["status_code"] != 200:
47 | """响应错误"""
48 | raise MAAFailedResponseException(details=data["status_code"])
49 |
50 | if not data["data"] or not data["data"]["total"] or not data["data"]["data"]:
51 | """没有作业"""
52 | raise MAANoResultException(details=params["document"])
53 |
54 | return data["data"]["data"]
55 |
56 |
57 | async def process_works(works: List[Dict], keyword: str) -> Optional[Tuple[BytesIO, int, str]]:
58 | """判断最新、加进数据库,返回图片内容 + 关卡id"""
59 | work = works[0]
60 | local_data = await MAACopilotSubsModel.filter(sub_keyword=keyword).first()
61 | local_upload_time = local_data.latest_upload_time
62 | latest_upload_time = datetime.fromisoformat(work["upload_time"]).timestamp()
63 |
64 | if latest_upload_time <= local_upload_time:
65 | return None
66 |
67 | content = json.loads(work["content"])
68 | operators = content["opers"]
69 |
70 | title, details, stage, operators_str = await process_copilot_data(work)
71 | await MAACopilotSubsModel.filter(sub_keyword=keyword).update(
72 | sub_groups=local_data.sub_groups,
73 | sub_keyword=local_data.sub_keyword,
74 | latest_upload_time=latest_upload_time,
75 | latest_id=work["id"],
76 | operators=operators,
77 | stage=stage,
78 | title=title,
79 | details=details
80 | )
81 |
82 | img_bytes = await build_result_image(title, details, stage, operators_str)
83 | return img_bytes, work["id"], local_data.sub_groups
84 |
85 |
86 | async def process_copilot_data(data: Dict) -> Tuple[str, str, str, str]:
87 | content = json.loads(data["content"])
88 | stage = content["stage_name"]
89 | stage = await stage_swap(stage, "code2name")
90 | stage = await handbook_stage_swap(stage, "code2name")
91 |
92 | title = content["doc"]["title"]
93 | details = content["doc"]["details"]
94 | operators = content["opers"]
95 | operators = [
96 | f"{o['name']}({o['skill']})"
97 | for o in operators
98 | ]
99 | operators_str = " ".join(operators)
100 | return title, details, stage, operators_str
101 |
102 |
103 | async def build_result_image(title: str, details: str, stage: str, operators_str: str) -> BytesIO:
104 | text = (
105 | f"[size=32][b][color=white]{title}[/color][/b][/size]\n"
106 | f"[size=16][color=white]作业简介: {details}[/color][/size]\n\n"
107 | f"[size=24][b][color=white]关卡名: {stage}[/color][/b][/size]\n\n"
108 | f"[size=16][b][color=white]阵容: {operators_str}[/color][/b][/size]"
109 | )
110 | img = text2image(
111 | padding=(20, 20, 20, 20),
112 | bg_color="black",
113 | text=text,
114 | max_width=560
115 | )
116 | img_bytes = BytesIO()
117 | img.save(img_bytes, format="png")
118 | return img_bytes
119 |
120 |
121 | async def add_maa_sub(group_id: str, keywords: str) -> str:
122 | result = await MAACopilotSubsModel.filter(sub_keyword=keywords).first()
123 | if not result:
124 | await MAACopilotSubsModel.create(sub_groups=group_id, sub_keyword=keywords)
125 | return f"{group_id}-{keywords} 已添加订阅!"
126 |
127 | if group_id in result.sub_groups:
128 | return f"{group_id}-{keywords} 已经添加过了!"
129 |
130 | await MAACopilotSubsModel.filter(sub_keyword=keywords).update(sub_groups=f"{result.sub_groups} {group_id}")
131 | return f"{group_id}-{keywords} 已添加订阅!"
132 |
133 |
134 | async def del_maa_sub(group_id: str, keywords: str) -> str:
135 | result = await MAACopilotSubsModel.filter(sub_keyword=keywords).first()
136 | if not result or group_id not in result.sub_groups:
137 | return f"{group_id}-{keywords} 没有订阅过哦!"
138 |
139 | if group_id == result.sub_groups: # 只有这一个群
140 | await result.delete()
141 | return f"{group_id}-{keywords} 已删除订阅!"
142 |
143 | groups = [_ for _ in result.sub_groups.split() if _ != group_id]
144 | await MAACopilotSubsModel.filter(sub_keyword=keywords).update(sub_groups=" ".join(groups))
145 | return f"{group_id}-{keywords} 已删除订阅!"
146 |
147 |
148 | async def que_maa_sub(group_id: str) -> str:
149 | result = await MAACopilotSubsModel.filter(sub_groups__contains=group_id).all()
150 | if not result:
151 | return f"{group_id} 尚未订阅过任何关键词哦!"
152 |
153 | answer = f"{group_id} 订阅过的关键词有: "
154 | for model in result:
155 | answer += f'\n{model.sub_keyword.replace("+", ", ")}'
156 |
157 | return answer
158 |
159 |
160 | class SubManager:
161 | def __init__(self):
162 | self.data: List["MAACopilotSubsModel"] = []
163 |
164 | async def reload_sub_data(self):
165 | """重载数据"""
166 | if not self.data:
167 | self.data = await MAACopilotSubsModel.all()
168 |
169 | async def random_sub_data(self):
170 | """随机获取一条数据"""
171 | if not self.data:
172 | return None
173 | sub = random.choice(self.data)
174 | self.data.remove(sub)
175 | if sub:
176 | return sub.sub_keyword
177 | await self.reload_sub_data()
178 | return await self.random_sub_data()
179 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_open_recruitment/__init__.py:
--------------------------------------------------------------------------------
1 | """公招筛选"""
2 | import httpx
3 | from nonebot import on_command, logger
4 | from nonebot.plugin import PluginMetadata
5 | from nonebot.params import Arg, RawCommand
6 | from nonebot.typing import T_State
7 | from nonebot.matcher import Matcher
8 | from nonebot.exception import ActionFailed
9 | from nonebot.adapters.onebot.v11 import Message, MessageSegment, MessageEvent
10 |
11 | from typing import Union
12 |
13 | from .data_source import BuildRecruitmentCard, process_word_tags, baidu_ocr
14 |
15 |
16 | recruit = on_command("公招", aliases={"公开招募"})
17 |
18 |
19 | @recruit.handle()
20 | async def _(state: T_State, event: MessageEvent, matcher: Matcher, raw: str = RawCommand()):
21 | if event.reply:
22 | event.message = event.reply.message
23 |
24 | if event.message.get("image", None): # 自带图片
25 | logger.debug("发送公招截图")
26 | for img in event.message["image"]:
27 | img_url = img.data.get("url", "")
28 | state["recruit"] = "image"
29 | matcher.set_arg("rec", img_url)
30 |
31 | elif event.message.extract_plain_text().replace(raw, "").strip(): # 文字tag
32 | tags = event.message.extract_plain_text().replace(raw, "").strip()
33 | logger.debug("直接输入文字标签")
34 | state["recruit"] = "str"
35 | matcher.set_arg("rec", tags)
36 |
37 |
38 | @recruit.got(key="rec", prompt="请发送公招截图:")
39 | async def _(state: T_State, rec: Union[Message, str] = Arg()):
40 | if state.get("recruit", None) == "str": # 文字输入
41 | tags = set(process_word_tags(rec.split()))
42 | else:
43 | if isinstance(rec, Message):
44 | img_url = rec["image"][0].data.get("url", "")
45 | else:
46 | img_url = rec
47 | await recruit.send("识别中...")
48 | async with httpx.AsyncClient() as client:
49 | try:
50 | tags = await baidu_ocr(image_url=img_url, client=client)
51 | except FileNotFoundError as e:
52 | logger.error("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
53 | await recruit.finish("公招标签文件缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
54 |
55 | if tags is None:
56 | await recruit.finish("百度OCR出错,请检查运行日志!", at_sender=True)
57 | if not tags:
58 | await recruit.finish("没有检测到符合要求的公招标签!", at_sender=True)
59 | logger.debug(f"tags: {tags}")
60 | await recruit.send(f"检测到的公招标签:{', '.join(list(tags))}")
61 |
62 | try:
63 | recruit_list = await BuildRecruitmentCard.build_target_characters(tags)
64 | except FileNotFoundError as e:
65 | logger.error("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
66 | await recruit.finish("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
67 |
68 | if not recruit_list:
69 | await recruit.finish("没有必出稀有干员的标签组合哦!", at_sender=True)
70 | draw = BuildRecruitmentCard(recruit_list)
71 | image = draw.build_main()
72 | img = MessageSegment.image(image)
73 | try:
74 | await recruit.finish(Message(img))
75 | except ActionFailed as e:
76 | await recruit.finish(f"图片发送失败:{e}")
77 |
78 |
79 | __plugin_meta__ = PluginMetadata(
80 | name="公开招募",
81 | description="查看公招标签可能出的稀有干员组合",
82 | usage=(
83 | "命令:"
84 | "\n 公招 [公招界面截图] => 查看标签组合及可能出现的干员"
85 | "\n 回复公招界面截图:公招 => 同上"
86 | "\n 公招 [标签1] [标签2] ... => 同上"
87 | ),
88 | extra={
89 | "name": "open_recruitment",
90 | "author": "NumberSir",
91 | "version": "0.1.0"
92 | }
93 | )
94 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_open_recruitment/data_source.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import httpx
4 | from aiofiles import open as aopen
5 | import json
6 | from typing import Set, List, Tuple, Dict, Any, Union
7 | from PIL import Image, ImageFont
8 | from PIL.ImageDraw import Draw
9 | from itertools import permutations
10 | import math
11 | from io import BytesIO
12 |
13 | from nonebot import get_driver, logger
14 |
15 | from ..core.models_v3 import Character
16 | from ..configs import BaiduOCRConfig, PathConfig
17 | from ..utils import text_border, get_recruitment_available
18 |
19 | bconfig = BaiduOCRConfig.parse_obj(get_driver().config.dict())
20 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
21 | font_path = Path(pcfg.arknights_font_path).absolute()
22 | gamedata_path = Path(pcfg.arknights_gamedata_path).absolute()
23 |
24 |
25 | async def baidu_ocr(image_url: str, client: httpx.AsyncClient) -> Set[str]:
26 | """百度ocr"""
27 | access_token = await get_baidu_ocr_access_token(client)
28 |
29 | url = f"https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token={access_token}"
30 | client.headers["Content-Type"] = "application/x-www-form-urlencoded"
31 | data = {"url": image_url}
32 | response = await client.post(url, data=data)
33 |
34 | try:
35 | all_words = {_["words"] for _ in response.json()["words_result"]}
36 | except KeyError as e:
37 | logger.error("百度ocr识别失败:")
38 | logger.error(f"{response.json()}")
39 | return None
40 |
41 | async with aopen(gamedata_path / "excel" / "gacha_table.json", "r", encoding="utf-8") as fp:
42 | tags = {_["tagName"] for _ in json.loads(await fp.read())["gachaTags"]}
43 |
44 | return {_ for _ in all_words if _ in tags}
45 |
46 |
47 | async def get_baidu_ocr_access_token(client: httpx.AsyncClient) -> str:
48 | """百度ocr获取token"""
49 | url = (
50 | f"https://aip.baidubce.com/oauth/2.0/token?"
51 | f"grant_type=client_credentials&"
52 | f"client_id={bconfig.arknights_baidu_api_key}&"
53 | f"client_secret={bconfig.arknights_baidu_secret_key}")
54 | response = await client.post(url=url)
55 | data = response.json()
56 | try:
57 | return data["access_token"]
58 | except KeyError as e:
59 | logger.warning("百度ocr获取token失败!")
60 | logger.warning(f"{data}")
61 |
62 |
63 | def process_word_tags(tags: list):
64 | """处理文字简化标签"""
65 | for idx, tag in enumerate(tags):
66 | if tag in {"高资", "高姿", "高级"}:
67 | tags[idx] = "高级资深干员"
68 | elif tag in {"资深", "资干"}:
69 | tags[idx] = "资深干员"
70 | elif tag in {"机械", "支机"}:
71 | tags[idx] = "支援机械"
72 | elif tag in {"近战", "远程"}:
73 | tags[idx] = f"{tags[idx]}位"
74 | elif tag in {"回费", "费回", "回复", "恢复"}:
75 | tags[idx] = "费用回复"
76 | elif tags[idx] in {"快活", "复活", "快速"}:
77 | tags[idx] = "快速复活"
78 | elif "术士" in tag:
79 | tags[idx] = tag.replace("术士", "术师")
80 | tag = tag.replace("术士", "术师")
81 |
82 | if tag in {"近卫", "狙击", "重装", "医疗", "辅助", "术师", "特种", "先锋", "男性", "女性"}:
83 | tags[idx] = f"{tags[idx]}干员"
84 | return tags
85 |
86 |
87 | class BuildRecruitmentCard:
88 | """绘图"""
89 | def __init__(self, result_groups: List[Dict[str, Any]]):
90 | self.result_groups = result_groups
91 | self.font_norm = ImageFont.truetype(str(font_path / "Arknights-zh.otf"), 24)
92 | self.font_small = ImageFont.truetype(str(font_path / "Arknights-zh.otf"), 20)
93 |
94 | self.result_images: List[Tuple[Image, int]] = []
95 |
96 | def build_group(self, result_group: Dict[str, Any]):
97 | """每一个组合绘制"""
98 | tags: List[str] = result_group["tags"]
99 | chts: List[Character] = result_group["chts"]
100 |
101 | rows = math.ceil(len(chts) / 6) # 每行最多6头像
102 | result_bg = Image.new("RGBA", (920, 152 * rows), (0, 0, 0, 0)) # tag 块是 152x152, 头图块是 128x(128+24)
103 |
104 | tag_bg = Image.new("RGBA", (152, 152), (50, 50, 50, 0)) # tag 块
105 | for idx, tag in enumerate(tags): # 一个tag组里的每个tag
106 | text_border(text=tag, draw=Draw(tag_bg), x=76, y=(15 + 30 * idx), anchor="mm", font=self.font_norm,
107 | fill_colour=(255, 255, 255, 255), shadow_colour=(0, 0, 0, 255))
108 | result_bg.paste(tag_bg, box=(0, 0), mask=tag_bg.split()[3])
109 |
110 | cht_bg = Image.new("RGBA", (920, 912), (50, 50, 50, 0))
111 | for idx, cht in enumerate(chts): # 给头像贴光
112 | cht.avatar = cht.avatar.resize((128, 128))
113 | if cht.rarity == 5: # 六星
114 | light = Image.new("RGBA", (128, 128), (255, 127, 39, 250))
115 | light.paste(cht.avatar, mask=cht.avatar.split()[3])
116 | cht.avatar = light
117 | elif cht.rarity == 4: # 五星
118 | light = Image.new("RGBA", (128, 128), (255, 201, 14, 250))
119 | light.paste(cht.avatar, mask=cht.avatar.split()[3])
120 | cht.avatar = light
121 | elif cht.rarity == 3: # 四星
122 | light = Image.new("RGBA", (128, 128), (216, 179, 216, 250))
123 | light.paste(cht.avatar, mask=cht.avatar.split()[3])
124 | cht.avatar = light
125 | elif cht.rarity == 0: # 一星
126 | light = Image.new("RGBA", (128, 128), (255, 255, 255, 250))
127 | light.paste(cht.avatar, mask=cht.avatar.split()[3])
128 | cht.avatar = light
129 |
130 | cht_bg.paste(cht.avatar, box=(128 * (idx - 6 * (idx // 6)), 0 + 152 * (idx // 6)),
131 | mask=cht.avatar.split()[3]) # 粘头像,每六个换行
132 | text_border(cht.name, Draw(cht_bg), x=(64 + 128 * (idx - 6 * (idx // 6))), y=(128 + 12 + 152 * (idx // 6)),
133 | anchor="mm", font=self.font_small, fill_colour=(255, 255, 255, 255),
134 | shadow_colour=(0, 0, 0, 255))
135 | result_bg.paste(cht_bg, box=(152, 0), mask=cht_bg.split()[3])
136 | return result_bg, rows # 记录每个结果及行数,方便绘制总图
137 |
138 | def build_main(self):
139 | """绘制总图"""
140 | self.result_images = [self.build_group(group) for group in self.result_groups]
141 | result_groups = self.sort_result_groups()
142 | columns = len(result_groups) # 总列数
143 |
144 | main_background = Image.new("RGBA", size=(10 * 2 + (920 + 24) * columns, 10 * 2 + 152 * 10),
145 | color=(50, 50, 50, 200))
146 | width = main_background.size[0]
147 | height = main_background.size[1]
148 |
149 | Draw(main_background).rectangle(xy=(0, 0, width, height), outline=(200, 200, 200), width=10)
150 |
151 | H = 0
152 | for idx_column, comb in enumerate(result_groups): # 每列
153 | H = 0
154 | if idx_column != 0:
155 | Draw(main_background).line(xy=(10 + (920 + 24) * idx_column, 0, 10 + (920 + 24) * idx_column, height),
156 | width=4, fill=(200, 200, 200))
157 | for idx_row, img_tuple in enumerate(comb): # 每行
158 | if idx_row != 0:
159 | Draw(main_background).line(
160 | xy=(10 + (920 + 24) * idx_column, 10 + H, 944 + 10 + (920 + 24) * idx_column, 10 + H), width=4,
161 | fill=(200, 200, 200))
162 | main_background.paste(im=img_tuple[0], box=(10 + (920 + 24) * idx_column, 10 + H),
163 | mask=img_tuple[0].split()[3])
164 | H += img_tuple[0].size[1]
165 |
166 | if columns == 1 and 0 < H < height - 10: # 只有一列,可能要裁图片
167 | main_background = main_background.crop(box=(0, 0, width, H + 16))
168 | Draw(main_background).line(xy=(0, H + 10, width, H + 10), width=10, fill=(200, 200, 200))
169 |
170 | output = BytesIO()
171 | main_background.save(output, format="png")
172 | return output
173 |
174 | def sort_result_groups(self) -> List:
175 | """
176 | 分组, 每一大列行数最多 10 行
177 | """
178 | result_counts = sorted(self.result_images, key=lambda tup: tup[1], reverse=True) # 按照行数排序
179 |
180 | already_in = []
181 | comb = []
182 | flag = False
183 | for idx1, result1 in enumerate(result_counts):
184 | result1: Tuple[Image, int] # 分组图片,行数
185 | if idx1 in already_in: # 单个分组的行数
186 | continue
187 |
188 | tmp_row = [result1[1]] # 方便判断
189 | tmp_idx = [idx1]
190 |
191 | if not result_counts[idx1 + 1:]:
192 | flag = True
193 | for idx2, result2 in enumerate(result_counts[idx1 + 1:]):
194 | flag = False
195 | if idx2 + idx1 + 1 in already_in: # 当列所有分组的总行数
196 | continue
197 | if result1[1] + result2[1] > 10: # 加起来超过10行
198 | continue
199 | elif result1[1] + result2[1] == 10: # 加起来刚好等于 10 行
200 | already_in += [idx1, idx2 + idx1 + 1]
201 | comb.append([idx1, idx2 + idx1 + 1])
202 | break
203 |
204 | if sum(tmp_row) + result2[1] > 10: # 加起来超过10行
205 | continue
206 | elif sum(tmp_row) + result2[1] == 10: # 加起来刚好等于 10 行
207 | already_in += [idx1, idx2 + idx1 + 1]
208 | comb.append(tmp_idx + [idx2 + idx1 + 1])
209 | break
210 |
211 | tmp_idx.append(idx2 + idx1 + 1)
212 | already_in += [idx1, idx2 + idx1 + 1]
213 | tmp_row.append(result2[1])
214 | flag = True
215 | if flag:
216 | comb.append(tmp_idx)
217 |
218 | return [
219 | [
220 | result_counts[idx]
221 | for idx in c
222 | ] for c in comb
223 | ]
224 |
225 | @staticmethod
226 | def build_combinations(tags: set) -> set:
227 | """构造所有可能的5tag集合"""
228 | result = []
229 | for i in range(1, 6):
230 | result += list(permutations(tags, i))
231 | return set(result)
232 |
233 | @staticmethod
234 | async def build_target_characters(tags: set) -> List[Dict[str, Union[str, List[Character]]]]:
235 | """tag-干员组合"""
236 | chts = [(await Character().init(_)) for _ in await get_recruitment_available()] # 所有可公招的干员
237 | combs = BuildRecruitmentCard.build_combinations(tags) # 所有可能的tag组合
238 | cht_tags = { # 这一条提出来,省了1k倍速度
239 | _.name: await _.get_tags_for_open_recruitment()
240 | for _ in chts
241 | }
242 |
243 | result = []
244 | for comb in combs:
245 | comb = sorted(list(comb))
246 | mapping = []
247 | for cht in chts:
248 | if cht.rarity == 5 and "高级资深干员" not in comb:
249 | continue
250 | if len(comb) <= len(cht_tags[cht.name]) \
251 | and set(comb).issubset(cht_tags[cht.name]) \
252 | and cht.name not in mapping:
253 | mapping.append(cht)
254 | if mapping and {"tags": comb, "chts": mapping} not in result:
255 | result.append({"tags": comb, "chts": mapping})
256 |
257 | result_ = []
258 | for r in result:
259 | flag = all(cht.rarity not in {1, 2} for cht in r["chts"])
260 | if flag:
261 | result_.append(r)
262 | return result_
263 |
264 |
265 | __all__ = [
266 | "BuildRecruitmentCard",
267 | "process_word_tags",
268 | "baidu_ocr"
269 | ]
270 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_operator_info/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 获取干员信息:
3 | 1. 技能 1~7 升级材料 √
4 | 2. 精英化材料 √
5 | 3. 技能专精材料 √
6 | 4. 模组升级材料 √
7 | 5. 模组任务
8 | 6. 基本信息: HandbookInfo
9 | """
10 | from nonebot import on_command, logger
11 | from nonebot.plugin import PluginMetadata
12 | from nonebot.params import CommandArg
13 | from nonebot.adapters.onebot.v11 import Message, MessageSegment
14 |
15 | from .data_source import BuildOperatorInfo
16 | from ..core.models_v3 import Character
17 | from ..exceptions import *
18 | from ..utils.general import nickname_swap
19 |
20 | operator_info = on_command("方舟干员", aliases={"干员"})
21 |
22 |
23 | @operator_info.handle()
24 | async def _(arg: Message = CommandArg()):
25 | name = arg.extract_plain_text().strip()
26 | if not name:
27 | await operator_info.finish()
28 |
29 | try:
30 | name = await nickname_swap(name)
31 | cht = await Character.parse_name(name)
32 | except NamedCharacterNotExistException as e:
33 | await operator_info.finish(e.msg, at_sender=True)
34 |
35 | try:
36 | img_bytes = await BuildOperatorInfo(cht=cht).build_whole_image()
37 | except FileNotFoundError as e:
38 | logger.error("干员信息缺失,请使用 “更新方舟素材” 命令更新游戏素材后重试")
39 | await operator_info.finish(f"缺失干员信息:{name}, 请使用 “更新方舟素材” 命令更新游戏素材后重试")
40 | await operator_info.finish(MessageSegment.image(img_bytes))
41 |
42 |
43 | __plugin_meta__ = PluginMetadata(
44 | name="干员信息",
45 | description="查看干员精英化、技能升级、技能专精、模组解锁需要的材料",
46 | usage=(
47 | "命令:"
48 | "\n 干员 [干员名称] => 查看对应干员的精英化、技能升级、技能专精、模组解锁需要的材料"
49 | ),
50 | extra={
51 | "name": "operator_info",
52 | "author": "NumberSir",
53 | "version": "0.1.0"
54 | }
55 | )
56 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_operator_info/data_source.py:
--------------------------------------------------------------------------------
1 | """
2 | 干员信息:
3 | 1. 头像、名称、立绘、代号、稀有度、职业、子职业、基础tag、位置、阵营
4 | 2. 血量、攻击、物防、法抗、费用、阻挡、再部署
5 | 3. 天赋
6 | 4. 潜能
7 | 5. 技能
8 | 6. 后勤
9 | 7. 精英化
10 | 8. 技能升级
11 | 9. 模组
12 | """
13 | from pathlib import Path
14 | from typing import Union
15 |
16 | from PIL import Image, ImageFont
17 | from PIL.ImageDraw import Draw
18 | from io import BytesIO
19 |
20 | from ..core.models_v3 import Character
21 | from ..utils.image import text_border
22 | from ..configs.path_config import PathConfig
23 |
24 | from nonebot import get_driver
25 |
26 |
27 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
28 | font_path = Path(pcfg.arknights_font_path).absolute()
29 | gameimage_path = Path(pcfg.arknights_gameimage_path).absolute()
30 | gamedata_path = Path(pcfg.arknights_gamedata_path).absolute()
31 | # pcfg = PathConfig()
32 |
33 |
34 | class BuildOperatorInfo:
35 | """作图"""
36 | def __init__(self, cht: Character, font_en: Union[str, Path] = font_path / "Arknights-en.ttf", font_zh: Union[str, Path] = font_path / "Arknights-zh.otf"):
37 | self._operator = cht
38 | self._font_en = font_en if isinstance(font_en, str) else font_en.__str__()
39 | self._font_zh = font_zh if isinstance(font_zh, str) else font_zh.__str__()
40 |
41 | @property
42 | def character(self) -> Character:
43 | return self._operator
44 |
45 | async def build_whole_image(self) -> BytesIO:
46 | all_skills_img: Image = await self._build_all_skills()
47 | equip_img: Image = await self._build_equips()
48 | elite_img: Image = await self._build_elite()
49 | skills_img: Image = await self._build_skills()
50 | skin_img: Image = await self._build_skin()
51 |
52 | main_background = Image.new(mode="RGBA", size=(1904, 768), color=(100, 100, 100, 200))
53 | Draw(main_background).rectangle(xy=(0, 0, 1904, 768), outline=(10, 10, 10), width=4) # 最外围边框
54 |
55 | if self.character.rarity < 2: # 只有精一立绘
56 | main_background.paste(im=skin_img, box=(-160, 0), mask=skin_img.split()[3])
57 | else:
58 | main_background.paste(im=skin_img, box=(-160, -140), mask=skin_img.split()[3])
59 | main_background.paste(im=all_skills_img, box=(800, 48), mask=all_skills_img.split()[3]) # 右上角
60 | main_background.paste(im=skills_img, box=(800, 312), mask=skills_img.split()[3]) # 右下角
61 | main_background.paste(im=elite_img, box=(320, 48), mask=elite_img.split()[3]) # 左上角
62 | main_background.paste(im=equip_img, box=(48, 312), mask=equip_img.split()[3]) # 左下角
63 |
64 | img_bytes = BytesIO()
65 | main_background.save(img_bytes, format="png")
66 | return img_bytes
67 |
68 | async def _build_all_skills(self) -> Image:
69 | """全技能升级"""
70 | font_en = ImageFont.truetype(self._font_en, 24)
71 | main_background = Image.new(mode="RGBA", size=(1056, 216), color=(235, 235, 235, 160)) # 底图
72 | if not self.character.has_skills: # 没技能
73 | return self._build_all_skills_unavailable(main_background)
74 |
75 | img_head_shadow = Image.new(mode="RGBA", size=(1056, 24), color=(175, 175, 175, 200)) # 顶部阴影
76 | main_background.paste(im=img_head_shadow, box=(0, 0), mask=img_head_shadow.split()[3])
77 |
78 | backgrounds = []
79 | for lvl, all_skills in enumerate(self.character.all_skill_level_up):
80 | background = Image.new(mode="RGBA", size=(352, 96), color=(235, 235, 235, 160)) # 底图
81 | icon_box = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
82 | rank_icon = Image.open(gameimage_path / "ui" / "rank" / f"{lvl+1}.png").convert("RGBA").resize((96, 96))
83 | icon_box.paste(rank_icon, mask=rank_icon.split()[3])
84 | background.paste(im=icon_box, box=(0, 0), mask=icon_box.split()[3])
85 | # text_border(text=f"{lvl}~{lvl + 1}", draw=Draw(background), x=48, y=60, font=font_en, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255)) # 顶部文字
86 | for idx, cost_item in enumerate(await all_skills.get_cost()):
87 | icon = self.resize(cost_item.icon, 64) # 材料图标大小
88 | text_border(text=str(cost_item.count), draw=Draw(icon), x=45, y=57, font=font_en, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255)) # 右下角的数字
89 | background.paste(im=icon, box=(112 + idx*80, 16), mask=icon.split()[3]) # 透明度粘贴(r,g,b,a)
90 | backgrounds.append(background)
91 |
92 | for idx, bg in enumerate(backgrounds):
93 | if idx < 3:
94 | main_background.paste(im=bg, box=(idx * 352, 24))
95 | else:
96 | main_background.paste(im=bg, box=((idx - 3) * 352, 120))
97 |
98 | font_zh = ImageFont.truetype(self._font_zh, 16)
99 | text_border(text="技 能 升 级", draw=Draw(main_background), x=528, y=20, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255)) # 顶部文字
100 |
101 | main_draw = Draw(main_background)
102 | main_draw.line(xy=(0, 0, 1056, 0), width=4, fill=(50, 50, 50))
103 | main_draw.line(xy=(0, 0, 0, 216), width=4, fill=(50, 50, 50))
104 | main_draw.line(xy=(1054, 0, 1054, 216), width=4, fill=(50, 50, 50))
105 | main_draw.line(xy=(0, 214, 1056, 214), width=4, fill=(50, 50, 50))
106 |
107 | main_draw.line(xy=(0, 24, 1056, 24), width=2, fill=(50, 50, 50))
108 | main_draw.line(xy=(0, 120, 1056, 120), width=2, fill=(50, 50, 50))
109 | main_draw.line(xy=(96, 24, 96, 216), width=2, fill=(50, 50, 50))
110 | main_draw.line(xy=(352, 24, 352, 216), width=2, fill=(50, 50, 50))
111 | main_draw.line(xy=(448, 24, 448, 216), width=2, fill=(50, 50, 50))
112 | main_draw.line(xy=(704, 24, 704, 216), width=2, fill=(50, 50, 50))
113 | main_draw.line(xy=(800, 24, 800, 216), width=2, fill=(50, 50, 50))
114 | return main_background
115 |
116 | def _build_all_skills_unavailable(self, bg: Image) -> Image:
117 | """没技能的图"""
118 | font_zh = ImageFont.truetype(self._font_zh, 48)
119 | Draw(bg).text(xy=(528, 132), anchor="ms", align="center", text="该干员无技能升级", font=font_zh, fill=(255, 255, 255, 255))
120 | return bg
121 |
122 | async def _build_equips(self) -> Image:
123 | """模组升级"""
124 | font_en = ImageFont.truetype(self._font_en, 24)
125 | font_zh = ImageFont.truetype(self._font_zh, 24)
126 | main_background = Image.new(mode="RGBA", size=(704, 408), color=(235, 235, 235, 160)) # 底图
127 | if not await self.character.has_equips():
128 | return self._build_equips_unavailable(main_background)
129 |
130 | img_head_shadow = Image.new(mode="RGBA", size=(704, 24), color=(175, 175, 175, 200)) # 顶部阴影
131 | main_background.paste(im=img_head_shadow, box=(0, 0), mask=img_head_shadow.split()[3])
132 |
133 | main_backgrounds = []
134 | for equip in await self.character.get_equips():
135 | equip_main_backgrounds = Image.new(mode="RGBA", size=(352, 384), color=(235, 235, 235, 160)) # 每个模组的底图
136 | if equip.type_icon == "original":
137 | equip_icon = Image.open(gameimage_path / "equip" / "icon" / "default.png").convert("RGBA").resize((96, 96))
138 | else:
139 | try:
140 | equip_icon = Image.open(gameimage_path / "equip" / "icon" / f"{equip.icon_id}.png").convert("RGBA").resize((96, 96))
141 | except FileNotFoundError as e:
142 | equip_icon = Image.new(mode="RGBA", size=(96, 96), color=(0, 0, 0, 0))
143 | icon_shadow = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
144 | icon_shadow.paste(im=equip_icon, box=(0, 0), mask=equip_icon.split()[3])
145 | equip_main_backgrounds.paste(im=icon_shadow, box=(0, 0), mask=icon_shadow.split()[3])
146 | text_border(text=equip.name, draw=Draw(equip_main_backgrounds), x=224, y=60, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255))
147 |
148 | backgrounds = []
149 | for idx, items in (await equip.get_item_cost()).items():
150 | background = Image.new(mode="RGBA", size=(352, 96), color=(235, 235, 235, 160)) # 小底图
151 | text_shadow = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
152 | background.paste(im=text_shadow, box=(0, 0), mask=text_shadow.split()[3])
153 |
154 | level_icon = equip.rank(lvl=idx+1).resize((96-8, 96-8))
155 | background.paste(im=level_icon, box=(4, 4), mask=level_icon.split()[3])
156 |
157 | for idx_, cost_item in enumerate(items):
158 | icon = self.resize(cost_item.icon, 64)
159 | if cost_item.count >= 10000:
160 | count = f"{cost_item.count / 10000:.0f}w"
161 | font = ImageFont.truetype(self._font_en, 14)
162 | text_border(text=str(count), draw=Draw(icon), x=45, y=52, font=font, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255))
163 | else:
164 | text_border(text=str(cost_item.count), draw=Draw(icon), x=45, y=57, font=font_en, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255))
165 | background.paste(im=icon, box=(112 + idx_*80, 16), mask=icon.split()[3])
166 | backgrounds.append(background)
167 |
168 | # 粘到大图上
169 | for idx, bg in enumerate(backgrounds):
170 | equip_main_backgrounds.paste(im=bg, box=(0, (idx + 1) * 96))
171 | main_backgrounds.append(equip_main_backgrounds)
172 |
173 | for idx, bg in enumerate(main_backgrounds):
174 | main_background.paste(im=bg, box=(idx * 352, 24))
175 | font_zh = ImageFont.truetype(self._font_zh, 16)
176 | text_border(text="模 组 升 级", draw=Draw(main_background), x=352, y=20, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255)) # 左侧文字
177 |
178 | main_draw = Draw(main_background)
179 | main_draw.line(xy=(0, 0, 702, 0), width=4, fill=(50, 50, 50))
180 | main_draw.line(xy=(0, 0, 0, 408), width=4, fill=(50, 50, 50))
181 | main_draw.line(xy=(702, 0, 702, 408), width=4, fill=(50, 50, 50))
182 | main_draw.line(xy=(0, 406, 704, 406), width=4, fill=(50, 50, 50))
183 |
184 | main_draw.line(xy=(0, 24, 704, 24), width=2, fill=(50, 50, 50))
185 |
186 | main_draw.line(xy=(0, 120, 704, 120), width=2, fill=(50, 50, 50))
187 | main_draw.line(xy=(0, 216, 704, 216), width=2, fill=(50, 50, 50))
188 | main_draw.line(xy=(0, 312, 704, 312), width=2, fill=(50, 50, 50))
189 |
190 | main_draw.line(xy=(96, 24, 96, 408), width=2, fill=(50, 50, 50))
191 | main_draw.line(xy=(352, 24, 352, 408), width=2, fill=(50, 50, 50))
192 | main_draw.line(xy=(448, 24, 448, 408), width=2, fill=(50, 50, 50))
193 | main_draw.line(xy=(704, 24, 704, 408), width=2, fill=(50, 50, 50))
194 | return main_background
195 |
196 | def _build_equips_unavailable(self, bg: Image) -> Image:
197 | """没模组"""
198 | font_zh = ImageFont.truetype(self._font_zh, 48)
199 | Draw(bg).text(xy=(352, 216), anchor="ms", align="center", text="该干员无模组", font=font_zh, fill=(255, 255, 255, 255))
200 | return bg
201 |
202 | async def _build_elite(self) -> Image:
203 | """精英化"""
204 | font_en = ImageFont.truetype(self._font_en, 24)
205 | main_background = Image.new(mode="RGBA", size=(432, 216), color=(235, 235, 235, 160)) # 底图
206 | if not self.character.can_evolve_1: # 无法精1
207 | return self._build_elite_unavailable(main_background)
208 |
209 | img_head_shadow = Image.new(mode="RGBA", size=(432, 24), color=(175, 175, 175, 200)) # 顶部阴影
210 | main_background.paste(im=img_head_shadow, box=(0, 0), mask=img_head_shadow.split()[3])
211 |
212 | phases = self.character.phases
213 | backgrounds = []
214 | for lvl, phase in enumerate(phases):
215 | if lvl == 0:
216 | continue
217 | background = Image.new(mode="RGBA", size=(432, 96), color=(235, 235, 235, 160)) # 底图
218 | icon_box = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
219 | background.paste(im=icon_box, box=(0, 0), mask=icon_box.split()[3])
220 | level_icon = Image.open(gameimage_path / "ui" / "elite" / f"{lvl}.png", mode="r").convert("RGBA")
221 | if lvl in [1, 2]:
222 | level_icon = level_icon.resize(size=(96, 93))
223 | background.paste(im=level_icon, box=(0, 0), mask=level_icon.split()[3])
224 | costs = await phase.get_elite_cost()
225 | item_count = 0
226 | for cost_item in costs:
227 | icon = cost_item.icon.resize(size=(64, 64)) # 材料图标大小
228 | count = cost_item.count
229 | if count >= 10000:
230 | count = f"{count / 10000:.0f}w"
231 | font = ImageFont.truetype(self._font_en, 14)
232 | text_border(text=str(count), draw=Draw(icon), x=45, y=52, font=font, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255)) # 右下角的数字
233 | elif count <= 0:
234 | continue
235 | else:
236 | text_border(text=str(count), draw=Draw(icon), x=45, y=57, font=font_en, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255)) # 右下角的数字
237 | background.paste(im=icon, box=(112 + item_count, 16), mask=icon.split()[3]) # 透明度粘贴(r,g,b,a)
238 | item_count += 80
239 | backgrounds.append(background)
240 |
241 | # 粘到大图上
242 | for idx, bg in enumerate(backgrounds):
243 | main_background.paste(im=bg, box=(0, 24 + idx * 96))
244 | font_zh = ImageFont.truetype(self._font_zh, 16)
245 | # Draw(main_background).text(xy=(216, 20), anchor="ms", align="center", text="精英化", font=font_zh, fill=(255, 255, 255, 255)) # 最顶部的字
246 | text_border(text="精 英 化", draw=Draw(main_background), x=216, y=20, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255)) # 左侧文字
247 |
248 | main_draw =Draw(main_background)
249 | main_draw.line(xy=(0, 0, 432, 0), width=4, fill=(50, 50, 50))
250 | main_draw.line(xy=(0, 0, 0, 216), width=4, fill=(50, 50, 50))
251 | main_draw.line(xy=(430, 0, 430, 216), width=4, fill=(50, 50, 50))
252 | main_draw.line(xy=(0, 214, 432, 214), width=4, fill=(50, 50, 50))
253 |
254 | main_draw.line(xy=(0, 24, 432, 24), width=2, fill=(50, 50, 50))
255 | main_draw.line(xy=(0, 120, 432, 120), width=2, fill=(50, 50, 50))
256 | main_draw.line(xy=(96, 24, 96, 216), width=2, fill=(50, 50, 50))
257 |
258 | return main_background
259 |
260 | def _build_elite_unavailable(self, bg: Image) -> Image:
261 | """没法精英化"""
262 | font_zh = ImageFont.truetype(self._font_zh, 48)
263 | Draw(bg).text(xy=(216, 132), anchor="ms", align="center", text="该干员无法精英化", font=font_zh, fill=(255, 255, 255, 255))
264 | return bg
265 |
266 | async def _build_skills(self) -> Image:
267 | """技能专精"""
268 | font_en = ImageFont.truetype(self._font_en, 24)
269 | font_zh = ImageFont.truetype(self._font_zh, 24)
270 | main_background = Image.new(mode="RGBA", size=(1056, 408), color=(235, 235, 235, 160)) # 底图
271 | if not self.character.can_skills_lvl_up: # 不能专精
272 | return self._build_skills_unavailable(main_background)
273 |
274 | img_head_shadow = Image.new(mode="RGBA", size=(1056, 24), color=(175, 175, 175, 200)) # 顶部阴影
275 | main_background.paste(im=img_head_shadow, box=(0, 0), mask=img_head_shadow.split()[3])
276 | main_backgrounds = []
277 | for skill in await self.character.get_skills():
278 | skill_main_backgrounds = Image.new(mode="RGBA", size=(352, 384), color=(235, 235, 235, 160)) # 每个技能的底图
279 |
280 | icon_shadow = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
281 | skill_main_backgrounds.paste(im=icon_shadow, box=(0, 0), mask=icon_shadow.split()[3])
282 |
283 | skill_icon = skill.icon.resize(size=(96, 96)) # 技能图标
284 | skill_main_backgrounds.paste(im=skill_icon, box=(0, 0))
285 |
286 | text_border(text=skill.name, draw=Draw(skill_main_backgrounds), x=224, y=60, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255))
287 |
288 | backgrounds = []
289 | for idx, cond in enumerate(skill.level_up_cost_condition):
290 | background = Image.new(mode="RGBA", size=(352, 96), color=(235, 235, 235, 160)) # 小底图
291 | text_shadow = Image.new(mode="RGBA", size=(96, 96), color=(205, 205, 205, 200)) # 左侧阴影
292 | background.paste(im=text_shadow, box=(0, 0), mask=text_shadow.split()[3])
293 |
294 | level_icon = skill.rank(idx+1).convert("RGBA").resize(size=(96, 96)) # 专精图标
295 | background.paste(im=level_icon, box=(0, 0), mask=level_icon.split()[3])
296 |
297 | for idx_, cost_item in enumerate(await cond.get_cost()):
298 | icon = self.resize(cost_item.icon, 64) # 材料图标大小
299 | text_border(text=str(cost_item.count), draw=Draw(icon), x=45, y=57, font=font_en, shadow_colour=(255, 255, 255, 255), fill_colour=(0, 0, 0, 255))
300 | background.paste(im=icon, box=(112 + 80*idx_, 16), mask=icon.split()[3])
301 |
302 | backgrounds.append(background)
303 |
304 | # 粘到大图上
305 | for idx, bg in enumerate(backgrounds):
306 | skill_main_backgrounds.paste(im=bg, box=(0, (idx + 1) * 96))
307 | main_backgrounds.append(skill_main_backgrounds)
308 |
309 | for idx, bg in enumerate(main_backgrounds):
310 | main_background.paste(im=bg, box=(idx * 352, 24))
311 | font_zh = ImageFont.truetype(self._font_zh, 16)
312 | text_border(text="技 能 专 精", draw=Draw(main_background), x=528, y=20, font=font_zh, shadow_colour=(0, 0, 0, 255), fill_colour=(255, 255, 255, 255)) # 左侧文字
313 |
314 | main_draw = Draw(main_background)
315 | main_draw.line(xy=(0, 0, 1056, 0), width=4, fill=(50, 50, 50))
316 | main_draw.line(xy=(0, 0, 0, 408), width=4, fill=(50, 50, 50))
317 | main_draw.line(xy=(1054, 0, 1054, 408), width=4, fill=(50, 50, 50))
318 | main_draw.line(xy=(0, 406, 1056, 406), width=4, fill=(50, 50, 50))
319 |
320 | main_draw.line(xy=(0, 24, 1056, 24), width=2, fill=(50, 50, 50))
321 |
322 | main_draw.line(xy=(0, 120, 1056, 120), width=2, fill=(50, 50, 50))
323 | main_draw.line(xy=(0, 216, 1056, 216), width=2, fill=(50, 50, 50))
324 | main_draw.line(xy=(0, 312, 1056, 312), width=2, fill=(50, 50, 50))
325 |
326 | main_draw.line(xy=(96, 24, 96, 408), width=2, fill=(50, 50, 50))
327 | main_draw.line(xy=(352, 24, 352, 408), width=2, fill=(50, 50, 50))
328 | main_draw.line(xy=(448, 24, 448, 408), width=2, fill=(50, 50, 50))
329 | main_draw.line(xy=(704, 24, 704, 408), width=2, fill=(50, 50, 50))
330 | main_draw.line(xy=(800, 24, 800, 408), width=2, fill=(50, 50, 50))
331 | return main_background
332 |
333 | def _build_skills_unavailable(self, bg: Image) -> Image:
334 | """不能专精的图"""
335 | font_zh = ImageFont.truetype(self._font_zh, 48)
336 | Draw(bg).text(xy=(528, 216), anchor="ms", align="center", text="该干员无技能专精", font=font_zh, fill=(255, 255, 255, 255))
337 | return bg
338 |
339 | async def _build_skin(self) -> Image:
340 | """立绘"""
341 | return self.character.skin.convert(mode="RGBA").resize((1176, 1176))
342 |
343 | async def _build_talent(self) -> Image:
344 | """天赋"""
345 | ...
346 |
347 | @staticmethod
348 | def resize(img: Image, size: int):
349 | w, h = img.size
350 | img = img.resize((int(w*size/h), size)) if w >= h else img.resize((size, int(h*size/h)))
351 | return img
352 |
353 |
354 | __all__ = [
355 | "BuildOperatorInfo"
356 | ]
357 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/tool_sanity_notify/__init__.py:
--------------------------------------------------------------------------------
1 | """理智恢复提醒"""
2 | from datetime import datetime
3 |
4 | import tortoise.exceptions
5 | from nonebot import on_command, get_bot, logger, get_driver
6 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent, Message, Bot, MessageSegment
7 | from nonebot.params import CommandArg
8 | from nonebot.plugin import PluginMetadata
9 | from nonebot_plugin_apscheduler import scheduler
10 |
11 | from ..configs.scheduler_config import SchedulerConfig
12 | from ..core.database import UserSanityModel
13 |
14 | scfg = SchedulerConfig.parse_obj(get_driver().config.dict())
15 |
16 | add_notify = on_command("理智提醒", aliases={"ADDSAN"})
17 | check_notify = on_command("理智查看", aliases={"CHECKSAN"})
18 |
19 |
20 | @add_notify.handle()
21 | async def _(event: MessageEvent, args: Message = CommandArg()):
22 | args = args.extract_plain_text().strip().split()
23 | uid = event.user_id
24 | gid = event.group_id if isinstance(event, GroupMessageEvent) else 0
25 | now = datetime.now()
26 |
27 | if not args:
28 | notify_time = datetime.fromtimestamp(now.timestamp() + 135 * 360, tz=now.tzinfo)
29 | data = await UserSanityModel.filter(gid=gid, uid=uid).first()
30 | if not data:
31 | await UserSanityModel.create(
32 | gid=gid, uid=uid, record_time=now, notify_time=notify_time, status=1
33 | )
34 | else:
35 | await UserSanityModel.filter(gid=gid, uid=uid).update(
36 | record_san=0, notify_san=135,
37 | record_time=now, notify_time=notify_time, status=1
38 | )
39 | elif len(args) == 2:
40 | record_san, notify_san = args
41 | notify_time = datetime.fromtimestamp(now.timestamp() + (int(notify_san) - int(record_san)) * 360, tz=now.tzinfo)
42 | data = await UserSanityModel.filter(gid=gid, uid=uid).first()
43 | if not data:
44 | await UserSanityModel.create(
45 | gid=gid, uid=uid, record_san=record_san, notify_san=notify_san,
46 | record_time=now, notify_time=notify_time, status=1
47 | )
48 | else:
49 | await UserSanityModel.filter(gid=gid, uid=uid).update(
50 | record_san=record_san, notify_san=notify_san,
51 | record_time=now, notify_time=notify_time, status=1
52 | )
53 | else:
54 | await add_notify.finish("小笨蛋,命令的格式是:“理智提醒 [当前理智] [回满理智]” 或 “理智提醒” 哦!")
55 |
56 | await add_notify.finish(f"记录成功!将在 {notify_time.__str__()[:-7]} 提醒博士哦!")
57 |
58 |
59 | @check_notify.handle()
60 | async def _(event: MessageEvent):
61 | uid = event.user_id
62 | gid = event.group_id if isinstance(event, GroupMessageEvent) else 0
63 |
64 | data = await UserSanityModel.filter(gid=gid, uid=uid, status=1).first()
65 | if not data:
66 | await check_notify.finish("小笨蛋,你还没有记录过理智提醒哦!")
67 |
68 | data = data.__dict__
69 |
70 | record_time: datetime = data["record_time"]
71 | notify_time: datetime = data["notify_time"]
72 | now = datetime.now(tz=record_time.tzinfo)
73 |
74 | elapsed_time = now - record_time
75 | remain_time = notify_time - now
76 |
77 | recoverd_san: int = elapsed_time.seconds // 360 if elapsed_time.seconds >= 360 else 0
78 | now_san: int = data["record_san"] + recoverd_san
79 |
80 | await check_notify.finish(f"距离理智恢复完毕还有 {remain_time.__str__()[:-7]},当前理智:{now_san}(+{recoverd_san})")
81 |
82 |
83 | @scheduler.scheduled_job(
84 | "interval",
85 | minutes=scfg.sanity_notify_interval,
86 | )
87 | async def _():
88 | if scfg.sanity_notify_switch:
89 | logger.debug("checking sanity...")
90 | try:
91 | bot: Bot = get_bot()
92 | except ValueError:
93 | pass
94 | else:
95 | now = datetime.now()
96 | try:
97 | data = await UserSanityModel.filter(notify_time__lt=now, status=1).all()
98 | except tortoise.exceptions.BaseORMException:
99 | logger.error("检查理智提醒失败,数据库未初始化")
100 | else:
101 | if data:
102 | for model in data:
103 | if model.gid:
104 | await bot.send_group_msg(
105 | group_id=model.gid,
106 | message=Message(MessageSegment.at(model.uid) + f"你的理智已经恢复到{model.notify_san}了哦!")
107 | )
108 | else:
109 | await bot.send_private_msg(
110 | user_id=model.uid,
111 | message=Message(MessageSegment.at(model.uid) + f"你的理智已经恢复到{model.notify_san}了哦!")
112 | )
113 | await UserSanityModel.filter(gid=model.gid, uid=model.uid).update(status=0)
114 |
115 |
116 | __plugin_meta__ = PluginMetadata(
117 | name="理智提醒",
118 | description="在理智回满时提醒用户",
119 | usage=(
120 | "命令:"
121 | "\n 理智提醒 => 默认记当前理智为0,回满到135时提醒"
122 | "\n 理智提醒 [当前理智] [回满理智] => 同上,不过手动指定当前理智与回满理智"
123 | "\n 理智查看 => 查看距离理智回满还有多久,以及当期理智为多少"
124 | ),
125 | extra={
126 | "name": "sanity_notify",
127 | "author": "NumberSir",
128 | "version": "0.1.0"
129 | }
130 | )
131 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """一些功能吧"""
2 | from .general import *
3 | from .image import *
4 | from .database import *
5 | from .update import *
6 |
7 | from nonebot import on_command, logger
8 | from nonebot.params import CommandArg
9 | from nonebot.plugin import PluginMetadata
10 | from nonebot.adapters.onebot.v11 import Message
11 | import httpx
12 |
13 |
14 | update_game_resource = on_command("更新方舟素材")
15 | init_db = on_command("更新方舟数据库")
16 |
17 |
18 | @update_game_resource.handle()
19 | async def _():
20 | await update_game_resource.send("开始更新游戏素材,视网络情况需5分钟左右……")
21 | try:
22 | async with httpx.AsyncClient() as client:
23 | await ArknightsGameData(client).download_files()
24 | await ArknightsDB.init_data(force=True)
25 | await ArknightsGameImage(client).download_files()
26 | except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException) as e:
27 | logger.error("下载方舟游戏素材请求出错或连接超时,请修改代理、重试或手动下载:")
28 | logger.error("https://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
29 | await update_game_resource.finish("下载方舟游戏素材请求出错或连接超时,请修改代理、重试或手动下载:\nhttps://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
30 | else:
31 | await update_game_resource.finish("游戏素材更新完成!")
32 |
33 |
34 | @init_db.handle()
35 | async def _(args: Message = CommandArg()):
36 | await update_game_resource.send("开始更新游戏数据库,视磁盘读写性能需1分钟左右……")
37 | if args.extract_plain_text().strip() == "-D":
38 | await ArknightsDB.drop_data()
39 | await update_game_resource.send("已彻底删除原表,开始重新写入数据库……")
40 | await ArknightsDB.init_db()
41 | else:
42 | await ArknightsDB.init_data(force=True)
43 | await update_game_resource.finish("游戏数据库更新完成!")
44 |
45 |
46 | __plugin_meta__ = PluginMetadata(
47 | name="杂项",
48 | description="查看指令列表、更新游戏素材、更新本地数据库",
49 | usage=(
50 | "命令:"
51 | "\n 方舟帮助 => 查看指令列表"
52 | "\n 更新方舟素材 => 从Github下载游戏素材(json数据与图片)"
53 | "\n 更新方舟数据库 => 更新本地sqlite数据库"
54 | "\n 更新方舟数据库 -D => 删除原数据库各表并重新写入"
55 | ),
56 | extra={
57 | "name": "update_plugin_data",
58 | "author": "NumberSir",
59 | "version": "0.1.0"
60 | }
61 | )
62 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/utils/database.py:
--------------------------------------------------------------------------------
1 | """数据库相关"""
2 | from pathlib import Path
3 |
4 | import tortoise.exceptions
5 | from tortoise import Tortoise
6 | from tortoise.models import Model
7 | from aiofiles import open as aopen
8 | import json
9 | import asyncio
10 | import re
11 | from aiofiles import os as aos
12 | from nonebot import get_driver, logger
13 |
14 | from ..configs.path_config import PathConfig
15 | from ..core.database import *
16 |
17 |
18 | driver = get_driver()
19 | pcfg = PathConfig.parse_obj(driver.config.dict())
20 | db_url = Path(pcfg.arknights_db_url).absolute()
21 | gamedata_path = Path(pcfg.arknights_gamedata_path).absolute()
22 | # pcfg = PathConfig()
23 |
24 | class ArknightsDB:
25 | """初始化"""
26 | @staticmethod
27 | async def init_db():
28 | """建库,建表"""
29 | logger.info("##### ARKNIGHTS-SQLITE CONNECTING ...")
30 | await asyncio.sleep(5)
31 | await aos.makedirs(db_url.parent, exist_ok=True)
32 | await Tortoise.init(
33 | {
34 | "connections": {
35 | "arknights": {
36 | "engine": "tortoise.backends.sqlite",
37 | "credentials": {
38 | "file_path": f"{db_url}"
39 | }
40 | }
41 | },
42 | "apps": {
43 | "arknights": {
44 | "models": [
45 | f"{GAME_SQLITE_MODEL_MODULE_NAME}",
46 | f"{PLUGIN_SQLITE_MODEL_MODULE_NAME}"
47 | ],
48 | "default_connection": "arknights"
49 | }
50 | },
51 | "timezone": "Asia/Shanghai"
52 | }
53 | )
54 | logger.info("===== ARKNIGHTS-SQLITE CONNECTED.")
55 | await Tortoise.generate_schemas(safe=True)
56 | await ArknightsDB.init_data()
57 |
58 | @staticmethod
59 | async def close_connection():
60 | logger.info("##### ARKNIGHTS-SQLITE CONNECTION CLOSING ...")
61 | await Tortoise.close_connections()
62 | logger.info("===== ARKNIGHTS-SQLITE CONNECTION CLOSED.")
63 |
64 | """ INIT DATA"""
65 | @staticmethod
66 | async def init_data(force: bool = False):
67 | """填充数据"""
68 | logger.info("##### ARKNIGHTS-SQLITE DATA ALL INITIATING ...")
69 | try:
70 | await ArknightsDB._init_building_buff(force)
71 | await ArknightsDB._init_character(force)
72 | await ArknightsDB._init_constance(force)
73 | await ArknightsDB._init_equip(force)
74 | await ArknightsDB._init_gacha_pool(force)
75 | await ArknightsDB._init_handbook_info(force)
76 | await ArknightsDB._init_handbook_stage(force)
77 | await ArknightsDB._init_item(force)
78 | await ArknightsDB._init_skill(force)
79 | await ArknightsDB._init_skin(force)
80 | await ArknightsDB._init_stage(force)
81 | except (tortoise.exceptions.OperationalError, tortoise.exceptions.FieldError) as e:
82 | logger.error(f"数据库初始化出错: {e}")
83 | await ArknightsDB.drop_data()
84 | await ArknightsDB.init_db()
85 | logger.info("===== ARKNIGHTS-SQLITE DATA ALL INITIATED")
86 |
87 | @staticmethod
88 | async def _init_building_buff(force: bool = False):
89 | if not await ArknightsDB.is_table_empty(BuildingBuffModel) and not force:
90 | logger.info("\t- BuildingBuff data already initiated.")
91 | return
92 | async with aopen(gamedata_path / "excel" / "building_data.json", "r", encoding="utf-8") as fp:
93 | data = await fp.read()
94 | data = json.loads(data)
95 | buff_data = data["buffs"]
96 | tasks = {
97 | BuildingBuffModel.update_or_create(**v)
98 | for k, v in buff_data.items()
99 | }
100 | await asyncio.gather(*tasks)
101 | logger.info("\t- BuildingBuff data initiated.")
102 | await ArknightsDB._init_building_workshop_formula(data)
103 |
104 | @staticmethod
105 | async def _init_building_workshop_formula(data: dict):
106 | data = data["workshopFormulas"]
107 | tasks = {
108 | WorkshopFormulaModel.update_or_create(**v)
109 | for _, v in data.items()
110 | }
111 | await asyncio.gather(*tasks)
112 | logger.info("\t- WorkshopFormula data initiated.")
113 |
114 | @staticmethod
115 | async def _init_character(force: bool = False):
116 | if not await ArknightsDB.is_table_empty(CharacterModel) and not force:
117 | logger.info("\t- Character data already initiated.")
118 | return
119 | async with aopen(gamedata_path / "excel" / "character_table.json", "r", encoding="utf-8") as fp:
120 | data = await fp.read()
121 | async with aopen(gamedata_path / "excel" / "char_patch_table.json", "r", encoding="utf-8") as fp:
122 | data_ = await fp.read()
123 | data = json.loads(data)
124 | data_ = json.loads(data_)["patchChars"]
125 | data_["char_1001_amiya2"]["name"] = "近卫阿米娅"
126 | data.update(data_)
127 |
128 | amiya = await CharacterModel.filter(charId="char_1001_amiya2", name='阿米娅').first()
129 | if amiya:
130 | await amiya.delete()
131 |
132 | tasks = set()
133 | for k, v in data.items():
134 | if "classicPotentialItemId" in v:
135 | tasks.add(CharacterModel.update_or_create(charId=k, **v))
136 | else:
137 | tasks.add(CharacterModel.update_or_create(charId=k, classicPotentialItemId=None, **v))
138 | await asyncio.gather(*tasks)
139 | logger.info("\t- Character data initiated.")
140 |
141 | @staticmethod
142 | async def _init_constance(force: bool = False):
143 | if not await ArknightsDB.is_table_empty(ConstanceModel) and not force:
144 | logger.info("\t- Constance data already initiated.")
145 | return
146 | async with aopen(gamedata_path / "excel" / "gamedata_const.json", "r", encoding="utf-8") as fp:
147 | data = await fp.read()
148 | data = json.loads(data)
149 | tasks = {
150 | ConstanceModel.update_or_create(
151 | maxLevel=data["maxLevel"],
152 | characterExpMap=data["characterExpMap"],
153 | characterUpgradeCostMap=data["characterUpgradeCostMap"],
154 | evolveGoldCost=data["evolveGoldCost"],
155 | attackMax=data["attackMax"],
156 | defMax=data["defMax"],
157 | hpMax=data["hpMax"],
158 | reMax=data["reMax"],
159 | )
160 | }
161 | await asyncio.gather(*tasks)
162 | logger.info("\t- Constance data initiated.")
163 |
164 | await ArknightsDB._init_constance_rich_text_style(data)
165 | await ArknightsDB._init_constance_term_description(data)
166 |
167 | @staticmethod
168 | async def _init_constance_rich_text_style(data: dict):
169 | tasks = {
170 | RichTextStyleModel.update_or_create(text=k, style=v)
171 | for k, v in data["richTextStyles"].items()
172 | }
173 | await asyncio.gather(*tasks)
174 | logger.info("\t\t- RichTextStyle data initiated.")
175 |
176 | @staticmethod
177 | async def _init_constance_term_description(data: dict):
178 | tasks = {
179 | TermDescriptionModel.update_or_create(**v)
180 | for k, v in data["termDescriptionDict"].items()
181 | }
182 | await asyncio.gather(*tasks)
183 | logger.info("\t\t- TermDescription data initiated.")
184 |
185 | @staticmethod
186 | async def _init_equip(force: bool = False):
187 | if not await ArknightsDB.is_table_empty(EquipModel) and not force:
188 | logger.info("\t- Equip data already initiated.")
189 | return
190 | async with aopen(gamedata_path / "excel" / "uniequip_table.json", "r", encoding="utf-8") as fp:
191 | data = await fp.read()
192 | data = json.loads(data)
193 | equip_dict = data["equipDict"]
194 | mission_list = data["missionList"]
195 | char_equip = data["charEquip"]
196 | tasks: set = {
197 | EquipModel.update_or_create(**v)
198 | for k, v in equip_dict.items()
199 | }
200 | await asyncio.gather(*tasks)
201 |
202 | tasks = {
203 | EquipModel.filter(uniEquipId=v["uniEquipId"]).update(**v)
204 | for k, v in mission_list.items()
205 | }
206 | await asyncio.gather(*tasks)
207 |
208 | tasks = set()
209 | for k, v in char_equip.items():
210 | tasks = tasks.union({
211 | EquipModel.filter(uniEquipId=i).update(character=k)
212 | for i in v
213 | })
214 |
215 | await asyncio.gather(*tasks)
216 | logger.info("\t- Equip data initiated")
217 |
218 | @staticmethod
219 | async def _init_gacha_pool(force: bool = False):
220 | if not await ArknightsDB.is_table_empty(GachaPoolModel) and not force:
221 | logger.info("\t- GachaPool data already initiated.")
222 | return
223 | async with aopen(gamedata_path / "excel" / "gacha_table.json", "r", encoding="utf-8") as fp:
224 | data = await fp.read()
225 | data = json.loads(data)["gachaPoolClient"]
226 | tasks = {
227 | GachaPoolModel.update_or_create(**pool)
228 | for pool in data
229 | }
230 | await asyncio.gather(*tasks)
231 | logger.info("\t- GachaPool data initiated")
232 |
233 | @staticmethod
234 | async def _init_handbook_info(force: bool = False):
235 | if not await ArknightsDB.is_table_empty(HandbookInfoModel) and not force:
236 | logger.info("\t- HandbookInfo data already initiated.")
237 | return
238 | async with aopen(gamedata_path / "excel" / "handbook_info_table.json", "r", encoding="utf-8") as fp:
239 | data = await fp.read()
240 | data = json.loads(data)["handbookDict"]
241 | tasks = {
242 | HandbookInfoModel.update_or_create(
243 | infoId=k,
244 | sex=re.findall(r"性别】\s*(.*?)\s*\n", v["storyTextAudio"][0]["stories"][0]["storyText"])[0].strip(),
245 | **v
246 | )
247 | for k, v in data.items()
248 | if "npc_" not in k
249 | }
250 | await asyncio.gather(*tasks)
251 | logger.info("\t- HandbookInfo data initiated.")
252 |
253 | @staticmethod
254 | async def _init_handbook_stage(force: bool = False):
255 | if not await ArknightsDB.is_table_empty(HandbookStageModel) and not force:
256 | logger.info("\t- HandbookStage data already initiated.")
257 | return
258 | async with aopen(gamedata_path / "excel" / "handbook_info_table.json", "r", encoding="utf-8") as fp:
259 | data = await fp.read()
260 | data = json.loads(data)["handbookStageData"]
261 | tasks = {
262 | HandbookStageModel.update_or_create(**v)
263 | for _, v in data.items()
264 | }
265 | await asyncio.gather(*tasks)
266 | logger.info("\t- HandbookStage data initiated.")
267 |
268 | @staticmethod
269 | async def _init_item(force: bool = False):
270 | if not await ArknightsDB.is_table_empty(ItemModel) and not force:
271 | logger.info("\t- Item data already initiated.")
272 | return
273 | async with aopen(gamedata_path / "excel" / "item_table.json", "r", encoding="utf-8") as fp:
274 | data = await fp.read()
275 | data = json.loads(data)["items"]
276 | tasks = {
277 | ItemModel.update_or_create(**v)
278 | for _, v in data.items()
279 | }
280 | await asyncio.gather(*tasks)
281 | logger.info("\t- Item data initiated")
282 |
283 | @staticmethod
284 | async def _init_skill(force: bool = False):
285 | if not await ArknightsDB.is_table_empty(SkillModel) and not force:
286 | logger.info("\t- Skill data already initiated.")
287 | return
288 | async with aopen(gamedata_path / "excel" / "skill_table.json", "r", encoding="utf-8") as fp:
289 | data = await fp.read()
290 | data = json.loads(data)
291 | tasks = {
292 | SkillModel.update_or_create(
293 | name=v["levels"][0]["name"],
294 | skillType=v["levels"][0]["skillType"],
295 | durationType=v["levels"][0]["durationType"],
296 | prefabId=v["levels"][0]["prefabId"],
297 | **v
298 | )
299 | for _, v in data.items()
300 | }
301 | await asyncio.gather(*tasks)
302 | logger.info("\t- Skill data initiated")
303 |
304 | @staticmethod
305 | async def _init_skin(force: bool = False):
306 | if not await ArknightsDB.is_table_empty(SkinModel) and not force:
307 | logger.info("\t- Skin data already initiated.")
308 | return
309 | async with aopen(gamedata_path / "excel" / "skin_table.json", "r", encoding="utf-8") as fp:
310 | data = await fp.read()
311 | data = json.loads(data)["charSkins"]
312 | tasks = {
313 | SkinModel.update_or_create(**v)
314 | for _, v in data.items()
315 | }
316 | await asyncio.gather(*tasks)
317 | logger.info("\t- Skin data initiated")
318 |
319 | @staticmethod
320 | async def _init_stage(force: bool = False):
321 | if not await ArknightsDB.is_table_empty(StageModel) and not force:
322 | logger.info("\t- Stage data already initiated.")
323 | return
324 | async with aopen(gamedata_path / "excel" / "stage_table.json", "r", encoding="utf-8") as fp:
325 | data = await fp.read()
326 | data = json.loads(data)["stages"]
327 |
328 | tasks = set()
329 | for _, v in data.items():
330 | if all("canUse" not in key and "extra" not in key for key in v):
331 | tasks.add(StageModel.update_or_create(**v))
332 | continue
333 |
334 | for key in v.copy():
335 | if "canUse" not in key and "extra" not in key:
336 | continue
337 | del v[key]
338 | tasks.add(StageModel.update_or_create(**v))
339 | await asyncio.gather(*tasks)
340 | logger.info("\t- Stage data initiated")
341 |
342 | """ DROP DATA """
343 | @staticmethod
344 | async def drop_data():
345 | """填充数据"""
346 | logger.warning("***** ARKNIGHTS-SQLITE DATA ALL DROPPING ...")
347 | await ArknightsDB._drop_building_buff()
348 | await ArknightsDB._drop_character()
349 | await ArknightsDB._drop_constance()
350 | await ArknightsDB._drop_equip()
351 | await ArknightsDB._drop_gacha_pool()
352 | await ArknightsDB._drop_handbook_info()
353 | await ArknightsDB._drop_handbook_stage()
354 | await ArknightsDB._drop_item()
355 | await ArknightsDB._drop_skill()
356 | await ArknightsDB._drop_skin()
357 | await ArknightsDB._drop_stage()
358 | logger.warning("***** ARKNIGHTS-SQLITE DATA ALL DROPPED")
359 |
360 | @staticmethod
361 | async def _drop_building_buff():
362 | await BuildingBuffModel.raw(f"DROP TABLE `{BuildingBuffModel.Meta.table}`")
363 | logger.info("\t- BuildingBuff table dropped")
364 |
365 | @staticmethod
366 | async def _drop_character():
367 | await CharacterModel.raw(f"DROP TABLE `{CharacterModel.Meta.table}`")
368 | logger.info("\t- Character table dropped")
369 |
370 | @staticmethod
371 | async def _drop_constance():
372 | await ConstanceModel.raw(f"DROP TABLE `{ConstanceModel.Meta.table}`")
373 | logger.info("\t- Constance table dropped")
374 |
375 | @staticmethod
376 | async def _drop_equip():
377 | await EquipModel.raw(f"DROP TABLE `{EquipModel.Meta.table}`")
378 | logger.info("\t- Equip table dropped")
379 |
380 | @staticmethod
381 | async def _drop_gacha_pool():
382 | await GachaPoolModel.raw(f"DROP TABLE `{GachaPoolModel.Meta.table}`")
383 | logger.info("\t- GachaPool table dropped")
384 |
385 | @staticmethod
386 | async def _drop_handbook_info():
387 | await HandbookInfoModel.raw(f"DROP TABLE `{HandbookInfoModel.Meta.table}`")
388 | logger.info("\t- HandbookInfo table dropped")
389 |
390 | @staticmethod
391 | async def _drop_handbook_stage():
392 | await HandbookStageModel.raw(f"DROP TABLE `{HandbookStageModel.Meta.table}`")
393 | logger.info("\t- HandbookStage table dropped")
394 |
395 | @staticmethod
396 | async def _drop_item():
397 | await ItemModel.raw(f"DROP TABLE `{ItemModel.Meta.table}`")
398 | logger.info("\t- Item table dropped")
399 |
400 | @staticmethod
401 | async def _drop_skill():
402 | await SkillModel.raw(f"DROP TABLE `{SkillModel.Meta.table}`")
403 | logger.info("\t- Skill table dropped")
404 |
405 | @staticmethod
406 | async def _drop_skin():
407 | await SkinModel.raw(f"DROP TABLE `{SkinModel.Meta.table}`")
408 | logger.info("\t- Skin table dropped")
409 |
410 | @staticmethod
411 | async def _drop_stage():
412 | await StageModel.raw(f"DROP TABLE `{StageModel.Meta.table}`")
413 | logger.info("\t- Stage table dropped")
414 |
415 | @staticmethod
416 | async def is_table_empty(model: Model) -> bool:
417 | """已经有数据就不要再 init 了"""
418 | count = await model.all().count()
419 | return count == 0
420 |
421 | @staticmethod
422 | async def is_insert_new_column(model: Model) -> bool:
423 | """改模型了"""
424 | try:
425 | await model.filter()
426 | except tortoise.exceptions.OperationalError as e:
427 | return True
428 | return False
429 |
430 | @driver.on_bot_connect # 不能 on_startup, 要先下资源再初始化数据库
431 | async def _init_db():
432 | try:
433 | await ArknightsDB.init_db()
434 | except FileNotFoundError as e:
435 | logger.error("初始化数据库失败:所需的数据文件未找到,请手动下载:")
436 | logger.error("https://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
437 | logger.warning("***** ARKNIGHTS-SQLITE DATA INITIATING FAILED")
438 | except tortoise.exceptions.BaseORMException as e:
439 | logger.error("初始化数据库失败:请检查是否与其它使用 Tortoise-ORM 的插件初始化冲突")
440 | logger.warning("***** ARKNIGHTS-SQLITE CONNECTING FAILED")
441 |
442 | @driver.on_bot_disconnect
443 | async def _close_db():
444 | await ArknightsDB.close_connection()
445 |
446 |
447 | __all__ = [
448 | "ArknightsDB",
449 |
450 | "_init_db",
451 | "_close_db"
452 | ]
453 |
454 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/utils/general.py:
--------------------------------------------------------------------------------
1 | """通用功能"""
2 | import json
3 | import os
4 | from pathlib import Path
5 | from typing import Union, Dict, List
6 | from nonebot import get_driver
7 | from aiofiles import open as aopen
8 | from nonebot import logger
9 |
10 | from ..configs import PathConfig
11 |
12 |
13 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
14 | data_path = Path(pcfg.arknights_data_path).absolute()
15 | gamedata_path = Path(pcfg.arknights_gamedata_path).absolute()
16 | # pcfg = PathConfig()
17 |
18 | CHARACTER_FILE = gamedata_path / "excel" / "character_table.json"
19 | ITEM_FILE = gamedata_path / "excel" / "item_table.json"
20 | SUB_PROF_FILE = gamedata_path / "excel" / "uniequip_table.json"
21 | EQUIP_FILE = SUB_PROF_FILE
22 | TEAM_FILE = gamedata_path / "excel" / "handbook_team_table.json"
23 | SWAP_PATH = data_path / "arknights" / "processed_data"
24 | GACHA_PATH = gamedata_path / "excel" / "gacha_table.json"
25 | STAGE_PATH = gamedata_path / "excel" / "stage_table.json"
26 | HANDBOOK_STAGE_PATH = gamedata_path / "excel" / "handbook_info_table.json"
27 |
28 |
29 | async def _name_code_swap(
30 | value: str,
31 | swap_file: Path,
32 | source_file: Path,
33 | type_: str = "name2code",
34 | *,
35 | layer: tuple = (0, None),
36 | name_key: str = "name",
37 | code_key: str = None,
38 | data: Union[Dict, List] = None
39 | ):
40 | """
41 | 草,写了一坨屎山参数,自己都看不懂了
42 |
43 | :param value: 值
44 | :param swap_file: 保存的映射文件名
45 | :param source_file: 源文件
46 | :param type_: name2code / code2name
47 | :param layer: 有些源文件不是一层映射,如 arknights_item_table,需要向深 1 层进入 "items",对应 (1, "items")
48 | :param name_key: 有些源文件表示名字的键不叫 name,如 uniequip_table 的叫 "subProfessionName",
49 | :param code_key: 有些文件要转换的代码名不是键名,如 handbook_stage 的在值中,名为 code
50 | :param data: 没有源文件的,直接用data写入
51 | :return:
52 | """
53 | if not value:
54 | return ""
55 |
56 | if swap_file.exists():
57 | async with aopen(swap_file, "r", encoding="utf-8") as fp:
58 | mapping = json.loads(await fp.read())
59 | return mapping[type_].get(value, value)
60 |
61 | if data:
62 | async with aopen(swap_file, "w", encoding="utf-8") as fp:
63 | await fp.write(json.dumps(data, ensure_ascii=False))
64 | return data[type_].get(value, value)
65 |
66 | os.makedirs(swap_file.parent, exist_ok=True)
67 | mapping = {"name2code": {}, "code2name": {}}
68 | async with aopen(source_file, "r", encoding="utf-8") as fp:
69 | data = json.loads(await fp.read())
70 | for i in range(layer[0]):
71 | data = data[layer[i+1]]
72 |
73 | codes = [_[code_key] for _ in data.values()] if code_key else list(data.keys())
74 | names = [_[name_key] for _ in data.values()]
75 | mapping["name2code"] = dict(zip(names, codes))
76 | mapping["code2name"] = dict(zip(codes, names))
77 | async with aopen(swap_file, "w", encoding="utf-8") as fp:
78 | await fp.write(json.dumps(mapping, ensure_ascii=False))
79 |
80 | return mapping[type_].get(value, value)
81 |
82 |
83 | async def character_swap(value: str, type_: str = "name2code") -> str:
84 | """干员的名字-id互相查询,默认名字查id"""
85 | swap_file = SWAP_PATH / "character_swap.json"
86 | source_file = CHARACTER_FILE
87 | return await _name_code_swap(value, swap_file, source_file, type_)
88 |
89 |
90 | async def item_swap(value: str, type_: str = "name2code") -> str:
91 | """物品的名字-id互相查询,默认名字查id"""
92 | swap_file = SWAP_PATH / "item_swap.json"
93 | source_file = ITEM_FILE
94 | return await _name_code_swap(value, swap_file, source_file, type_, layer=(1, "items"))
95 |
96 |
97 | async def sub_prof_swap(value: str, type_: str = "name2code") -> str:
98 | """子职业的名字-id互相查询,默认名字查id"""
99 | swap_file = SWAP_PATH / "sub_prof_swap.json"
100 | source_file = SUB_PROF_FILE
101 | return await _name_code_swap(value, swap_file, source_file, type_, layer=(1, "subProfDict"), name_key="subProfessionName")
102 |
103 |
104 | async def equip_swap(value: str, type_: str = "name2code") -> str:
105 | """模组的名字-id互相查询,默认名字查id"""
106 | swap_file = SWAP_PATH / "equip_swap.json"
107 | source_file = EQUIP_FILE
108 | return await _name_code_swap(value, swap_file, source_file, type_, layer=(1, "equipDict"), name_key="uniEquipName")
109 |
110 |
111 | async def faction_swap(value: str, type_: str = "name2code") -> str:
112 | """阵营的名字-id互相查询,默认名字查id"""
113 | swap_file = SWAP_PATH / "faction_swap.json"
114 | source_file = TEAM_FILE
115 | return await _name_code_swap(value, swap_file, source_file, type_, name_key="powerName")
116 |
117 |
118 | async def prof_swap(value: str, type_: str = "name2code") -> str:
119 | """职业的名字-id互相查询,默认名字查id"""
120 | data = {
121 | "name2code": {
122 | "先锋干员": "PIONEER",
123 | "近卫干员": "WARRIOR",
124 | "狙击干员": "SNIPER",
125 | "治疗干员": "MEDIC",
126 | "重装干员": "TANK",
127 | "术师干员": "CASTER",
128 | "辅助干员": "SUPPORT",
129 | "特种干员": "SPECIAL"
130 | },
131 | "code2name": {
132 | "PIONEER": "先锋干员",
133 | "WARRIOR": "近卫干员",
134 | "SNIPER": "狙击干员",
135 | "MEDIC": "治疗干员",
136 | "TANK": "重装干员",
137 | "CASTER": "术师干员",
138 | "SUPPORT": "辅助干员",
139 | "SPECIAL": "特种干员"
140 | }
141 | }
142 | return data[type_][value]
143 |
144 |
145 | async def gacha_rule_swap(value: str, type_: str = "name2code") -> str:
146 | """池子类型"""
147 | data = {
148 | "name2code": {
149 | "春节": "ATTAIN",
150 | "限定": "LIMITED",
151 | "联动": "LINKAGE",
152 | "普通": "NORMAL"
153 | },
154 | "code2name": {
155 | "ATTAIN": "春节",
156 | "LIMITED": "限定",
157 | "LINKAGE": "联动",
158 | "NORMAL": "普通"
159 | }
160 | }
161 | return data[type_][value]
162 |
163 |
164 | async def stage_swap(value: str, type_: str = "name2code") -> str:
165 | """关卡的名字-id互相查询,默认名字查id"""
166 | swap_file = SWAP_PATH / "stage_swap.json"
167 | source_file = STAGE_PATH
168 | return await _name_code_swap(value, swap_file, source_file, type_, layer=(1, "stages"))
169 |
170 |
171 | async def handbook_stage_swap(value: str, type_: str = "name2code") -> str:
172 | """悖论模拟的名字-id互相查询,默认名字查id"""
173 | swap_file = SWAP_PATH / "handbook_stage_swap.json"
174 | source_file = HANDBOOK_STAGE_PATH
175 | return await _name_code_swap(value, swap_file, source_file, type_, layer=(1, "handbookStageData"), code_key="code")
176 |
177 |
178 | async def nickname_swap(value: str) -> str:
179 | """干员昵称/外号转换"""
180 | swap_file = SWAP_PATH / "nicknames.json"
181 | async with aopen(swap_file, "r", encoding="utf-8") as fp:
182 | data = json.loads(await fp.read())
183 |
184 | for k, v in data.items():
185 | if value == k or value in v:
186 | logger.info(f"{value} -> {k}")
187 | return k
188 | return value
189 |
190 |
191 | async def get_recruitment_available() -> List[str]:
192 | """获取可以公招获取的干员id们"""
193 | async with aopen(GACHA_PATH, "r", encoding="utf-8") as fp:
194 | text = json.loads(await fp.read())["recruitDetail"]
195 |
196 | # 处理这堆字
197 | text = text.replace("\\n", "\n").replace("<@rc.eml>", "\n").replace(">", "\n").split("\n")
198 | text = [_ for _ in text if _ and "<" not in _ and "--" not in _ and "★" not in _ and _ != " / "][1:]
199 | text = [" ".join(_.split(" / ")) for _ in text]
200 | text = " ".join(_.strip() for _ in text).split()
201 | result = [await character_swap(_) for _ in text]
202 | return result
203 |
204 |
205 | __all__ = [
206 | "character_swap",
207 | "item_swap",
208 | "sub_prof_swap",
209 | "equip_swap",
210 | "prof_swap",
211 | "faction_swap",
212 | "gacha_rule_swap",
213 | "stage_swap",
214 | "handbook_stage_swap",
215 |
216 | "nickname_swap",
217 |
218 | "get_recruitment_available"
219 | ]
220 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/utils/image.py:
--------------------------------------------------------------------------------
1 | from PIL import ImageDraw, ImageFont, Image
2 |
3 |
4 | def text_border(text: str, draw: ImageDraw, x: int, y: int, font: ImageFont, shadow_colour: tuple, fill_colour: tuple, anchor: str = "ms"):
5 | """文字加边框"""
6 | draw.text((x - 1, y), text=text, anchor=anchor, font=font, fill=shadow_colour)
7 | draw.text((x + 1, y), text=text, anchor=anchor, font=font, fill=shadow_colour)
8 | draw.text((x, y - 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
9 | draw.text((x, y + 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
10 |
11 | draw.text((x - 1, y - 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
12 | draw.text((x + 1, y - 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
13 | draw.text((x - 1, y + 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
14 | draw.text((x + 1, y + 1), text=text, anchor=anchor, font=font, fill=shadow_colour)
15 |
16 | draw.text((x, y), text=text, anchor=anchor, font=font, fill=fill_colour)
17 |
18 |
19 | def round_corner(img: Image, radius: int = 30):
20 | """圆角"""
21 | circle = Image.new("L", (radius*2, radius*2), 0) # 黑色背景
22 | draw = ImageDraw.Draw(circle)
23 | draw.ellipse((0, 0, radius*2, radius*2), fill=255) # 白色圆形
24 |
25 | img = img.convert("RGBA")
26 | w, h = img.size
27 |
28 | # 切圆
29 | alpha = Image.new("L", img.size, 255)
30 | lu = circle.crop((0, 0, radius, radius))
31 | ru = circle.crop((radius, 0, radius*2, radius))
32 | ld = circle.crop((0, radius, radius, radius*2))
33 | rd = circle.crop((radius, radius, radius*2, radius*2))
34 | alpha.paste(lu, (0, 0)) # 左上角
35 | alpha.paste(ru, (w-radius, 0)) # 右上角
36 | alpha.paste(ld, (0, h-radius)) # 左下角
37 | alpha.paste(rd, (w-radius, h-radius)) # 右下角
38 |
39 | img.putalpha(alpha)
40 | return img
41 |
42 |
43 | __all__ = [
44 | "text_border",
45 | "round_corner"
46 | ]
47 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/src/utils/update.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from pathlib import Path
3 | from typing import List, Dict
4 | from aiofiles import open as aopen, os as aos
5 | from lxml import etree
6 | from urllib.parse import quote, unquote
7 | from nonebot import logger, get_driver
8 |
9 | import httpx
10 |
11 | from ..configs import PathConfig, ProxyConfig, SchedulerConfig
12 |
13 | driver = get_driver()
14 | pcfg = PathConfig.parse_obj(get_driver().config.dict())
15 | data_path = Path(pcfg.arknights_data_path).absolute()
16 | gamedata_path = Path(pcfg.arknights_gamedata_path).absolute()
17 | gameimage_path = Path(pcfg.arknights_gameimage_path).absolute()
18 | font_path = Path(pcfg.arknights_font_path).absolute()
19 |
20 | xcfg = ProxyConfig.parse_obj(get_driver().config.dict())
21 | BASE_URL_RAW = xcfg.github_raw # 镜像
22 | BASE_URL_SITE = xcfg.github_site
23 |
24 | scfg = SchedulerConfig.parse_obj(get_driver().config.dict())
25 |
26 | REPOSITORIES = {
27 | "gamedata": "/yuanyan3060/ArknightsGameResource/master",
28 | "gameimage_1": "/yuanyan3060/Arknights-Bot-Resource/master",
29 | "gameimage_2": "/Aceship/Arknight-Images/master",
30 | }
31 |
32 | FILES = {
33 | "gamedata": [
34 | "gamedata/excel/building_data.json", # 基建技能,制造配方
35 | "gamedata/excel/char_patch_table.json", # 升变阿米娅
36 | "gamedata/excel/character_table.json", # 干员表
37 | "gamedata/excel/data_version.txt", # 数据版本
38 | "gamedata/excel/gamedata_const.json", # 游戏常数
39 | "gamedata/excel/gacha_table.json", # 公招相关
40 | "gamedata/excel/item_table.json", # 物品表
41 | "gamedata/excel/handbook_info_table.json", # 档案表
42 | "gamedata/excel/skill_table.json", # 技能表
43 | "gamedata/excel/uniequip_table.json", # 模组表、子职业映射
44 | "gamedata/excel/handbook_team_table.json", # 干员阵营
45 | "gamedata/excel/skin_table.json", # 皮肤
46 | "gamedata/excel/stage_table.json", # 关卡
47 | ]
48 | }
49 |
50 | DIRS = {
51 | "gamedata": [
52 | "/zh_CN/gamedata/excel"
53 | ],
54 | "gameimage_1": [
55 | "avatar",
56 | "item",
57 | "skill",
58 | "skin"
59 | ],
60 | "gameimage_2": [
61 | # "avatars", # 头像 (180x180)
62 | # "characters", # 立绘 (1024x1024 | 2048x2048)
63 | "classes", # 职业图标 (255x255)
64 | "equip/icon", # 模组图标 (511x511)
65 | "equip/stage", # 模组阶段图标 (174x160)
66 | "equip/type", # 模组分类图标
67 | "factions", # 阵营 (510x510)
68 | # "items", # 物品图标
69 | # "material", # 材料图标
70 | # "material/bg", # 材料背景图标 (190x190)
71 | "portraits", # 画像 (180x360)
72 | # "skills", # 技能图标 (128x128)
73 | "ui/chara", # 公招出货表层贴图
74 | "ui/elite", # 精英化图标
75 | "ui/infrastructure", # 基建技能分类图标
76 | "ui/infrastructure/skill", # 基建技能图标
77 | "ui/potential", # 潜能图标
78 | "ui/rank", # 专精图标、技能升级图标
79 | "ui/subclass", # 子职业图标
80 | ]
81 | }
82 |
83 |
84 | class ArknightsGameData:
85 | def __init__(self, client: httpx.AsyncClient = None):
86 | self._url = f"{BASE_URL_RAW}{REPOSITORIES['gamedata']}"
87 | self._client = client or httpx.AsyncClient()
88 |
89 | async def get_local_version(self) -> str:
90 | """获取本地版本"""
91 | try:
92 | async with aopen(gamedata_path / "excel" / "data_version.txt") as fp:
93 | data = await fp.read()
94 | except FileNotFoundError as e:
95 | return ""
96 | return data.split(":")[-1].strip("\n").strip()
97 |
98 | async def get_latest_version(self) -> str:
99 | """获取最新版本"""
100 | url = f"{self._url}/gamedata/excel/data_version.txt"
101 | response = await self._client.get(url, follow_redirects=True)
102 | return response.text.split(":")[-1].strip("\n").strip() # eg: "31.4.0"
103 |
104 | async def is_update_needed(self) -> bool:
105 | """是否要更新"""
106 | return await self.get_local_version() != await self.get_latest_version()
107 |
108 | async def download_files(self):
109 | """下载gamedata"""
110 | tmp = gamedata_path / "excel"
111 | await aos.makedirs(tmp, exist_ok=True)
112 | logger.info("##### ARKNIGHTS GAMEDATA DOWNLOAD BEGIN ")
113 |
114 | tasks = [
115 | self.save(self._url, file, tmp)
116 | for file in FILES['gamedata']
117 | ]
118 | await asyncio.gather(*tasks)
119 | logger.info("===== ARKNIGHTS GAMEDATA DOWNLOAD DONE ")
120 |
121 | async def save(self, url: str, file: str, tmp: Path):
122 | """异步gather用"""
123 | content = (await self._client.get(f"{url}/{file}", timeout=100, follow_redirects=True)).content
124 | async with aopen(tmp / file.split('/')[-1], "wb") as fp:
125 | await fp.write(content)
126 | logger.info(f"\t- Arknights-Data downloaded: {file.split('/')[-1]}")
127 |
128 |
129 | class ArknightsGameImage:
130 | def __init__(self, client: httpx.AsyncClient = None):
131 | self._client = client or httpx.AsyncClient()
132 | self._urls: List[str] = []
133 | self._htmls: Dict[str, str] = {}
134 |
135 | async def download_files(self):
136 | """下载gameimage"""
137 | tmp = gameimage_path
138 | await aos.makedirs(tmp, exist_ok=True)
139 | logger.info("##### ARKNIGHTS GAMEIMAGE DOWNLOAD BEGIN ")
140 |
141 | logger.info("\t### REQUESTING FILE LISTS ... ")
142 | tasks = []
143 | for dir_ in DIRS['gameimage_1']:
144 | await aos.makedirs(tmp / dir_, exist_ok=True)
145 | url = f"{BASE_URL_SITE}/yuanyan3060/Arknights-Bot-Resource/file-list/main/{dir_}"
146 | tasks.append(self.get_htmls(url, dir_))
147 | # logger.info(f"\t\t# REQUESTING {url} ... ")
148 | for dir_ in DIRS['gameimage_2']:
149 | await aos.makedirs(tmp / dir_, exist_ok=True)
150 | url = f"{BASE_URL_SITE}/Aceship/Arknight-Images/file-list/main/{dir_}"
151 | tasks.append(self.get_htmls(url, dir_))
152 | # logger.info(f"\t\t# REQUESTING {url} ... ")
153 | await asyncio.gather(*tasks)
154 |
155 | logger.info("\t### REQUESTING REPOS ... ")
156 | for dir_, (html, url) in self._htmls.items():
157 | # logger.info(f"\t\t# REQUESTING {url} ... ")
158 | dom = etree.HTML(html, etree.HTMLParser())
159 | file_names: List[str] = dom.xpath(
160 | "//a[@class='js-navigation-open Link--primary']/text()"
161 | )
162 | if REPOSITORIES["gameimage_1"].split("/")[1] in url:
163 | if "item" in dir_:
164 | self._urls.extend(
165 | f"{BASE_URL_RAW}{REPOSITORIES['gameimage_1']}/{dir_}/{file_name}"
166 | for file_name in file_names
167 |
168 | if "recruitment" not in file_name
169 | # and "token_" not in file_name
170 | and "ap_" not in file_name
171 | and "clue_" not in file_name
172 | and "itempack_" not in file_name
173 | and "LIMITED_" not in file_name
174 | and "LMTGS_" not in file_name
175 | and "p_char_" not in file_name
176 | and "randomMaterial" not in file_name
177 | and "tier" not in file_name
178 | )
179 | elif "avatar" in dir_:
180 | self._urls.extend(
181 | f"{BASE_URL_RAW}{REPOSITORIES['gameimage_1']}/{dir_}/{file_name}"
182 | for file_name in file_names
183 |
184 | if "#" not in file_name # 不要皮肤,太大了
185 | and "char" in file_name
186 | and "+.png" not in file_name
187 | )
188 | elif "skin" in dir_:
189 | self._urls.extend(
190 | f"{BASE_URL_RAW}{REPOSITORIES['gameimage_1']}/{dir_}/{file_name}"
191 | for file_name in file_names
192 |
193 | if "#" not in file_name # 不要皮肤,太大了
194 | )
195 | elif "skill" in dir_:
196 | self._urls.extend(
197 | f"{BASE_URL_RAW}{REPOSITORIES['gameimage_1']}/{dir_}/{file_name}"
198 | for file_name in file_names
199 | )
200 | elif REPOSITORIES["gameimage_2"].split("/")[1] in url:
201 | self._urls.extend(
202 | f"{BASE_URL_RAW}{REPOSITORIES['gameimage_2']}/{dir_}/{file_name}"
203 | for file_name in file_names
204 | )
205 |
206 | tasks = [self.save(url, tmp) for url in self._urls if not (tmp / url.split('/master/')[-1]).exists()]
207 | await asyncio.gather(*tasks)
208 | logger.info("===== ARKNIGHTS GAMEIMAGE DOWNLOAD DONE ")
209 |
210 | async def get_htmls(self, url: str, dir_: str):
211 | """异步gather用"""
212 | html = (await self._client.get(url, timeout=100, follow_redirects=True)).text
213 | self._htmls[dir_] = (html, url)
214 |
215 | async def save(self, url: str, tmp: Path):
216 | """异步gather用"""
217 | # print(url)
218 | content = (await self._client.get(quote(url, safe="/:"), timeout=100, follow_redirects=True)).content
219 | if not url.endswith(".png"):
220 | return
221 | async with aopen(tmp / unquote(url).split('/master/')[-1], "wb") as fp:
222 | await fp.write(content)
223 | logger.info(f"\t- Arknights-Image downloaded: {unquote(url).split('/master/')[-1]}")
224 |
225 |
226 | async def download_extra_files(client: httpx.AsyncClient):
227 | """下载猜干员的图片素材、干员外号昵称"""
228 | urls = [
229 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/guess_character/correct.png",
230 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/guess_character/down.png",
231 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/guess_character/up.png",
232 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/guess_character/vague.png",
233 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/guess_character/wrong.png",
234 |
235 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/arknights/processed_data/nicknames.json",
236 | ]
237 | logger.info("##### EXTRA FILES DOWNLOAD BEGIN")
238 | await aos.makedirs(data_path / "guess_character", exist_ok=True)
239 | await aos.makedirs(data_path / "arknights/processed_data", exist_ok=True)
240 | for url in urls:
241 | path = url.split("/data/")[-1]
242 | if (data_path / path).exists():
243 | continue
244 | response = await client.get(url, follow_redirects=True)
245 | async with aopen(data_path / path, "wb") as fp:
246 | await fp.write(response.content)
247 | logger.info(f"\t- Extra file downloaded: {path}")
248 | await download_fonts(client)
249 | logger.info("===== EXTRA FILES DOWNLOAD DONE")
250 |
251 |
252 | async def download_fonts(client: httpx.AsyncClient):
253 | """下载字体"""
254 | urls = [
255 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/fonts/Arknights-en.ttf",
256 | f"{BASE_URL_RAW}/NumberSir/nonebot_plugin_arktools/main/nonebot_plugin_arktools/data/fonts/Arknights-zh.otf",
257 | ]
258 | await aos.makedirs(font_path, exist_ok=True)
259 | for url in urls:
260 | path = url.split("/")[-1]
261 | if (font_path / path).exists():
262 | continue
263 | response = await client.get(url, follow_redirects=True)
264 | async with aopen(font_path / path, "wb") as fp:
265 | await fp.write(response.content)
266 | logger.info(f"\t- Font file downloaded: {path}")
267 |
268 |
269 | @driver.on_startup
270 | async def _init_game_files():
271 | if scfg.arknights_update_check_switch:
272 | async with httpx.AsyncClient(timeout=100) as client:
273 | try:
274 | await download_extra_files(client)
275 | except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException) as e:
276 | logger.error("下载方舟额外素材请求出错或连接超时,请修改代理、重试或手动下载:")
277 | logger.error("https://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
278 |
279 | logger.info("检查方舟游戏素材版本中 ...")
280 | is_latest = False
281 | try:
282 | if not await ArknightsGameData(client).is_update_needed():
283 | logger.info("方舟游戏素材当前为最新!")
284 | is_latest = True
285 | except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException) as e:
286 | logger.error("检查方舟素材版本请求出错或连接超时,请修改代理、重试或手动下载:")
287 | logger.error("https://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
288 | else:
289 | if not is_latest:
290 | logger.info("方舟游戏素材需要更新,开始下载素材...")
291 | try:
292 | await ArknightsGameData(client).download_files()
293 | await ArknightsGameImage(client).download_files()
294 | except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException) as e:
295 | logger.error("下载方舟素材请求出错或连接超时,请修改代理、重试或手动下载:")
296 | logger.error("https://github.com/NumberSir/nonebot_plugin_arktools#%E5%90%AF%E5%8A%A8%E6%B3%A8%E6%84%8F")
297 |
298 |
299 | __all__ = [
300 | "ArknightsGameImage",
301 | "ArknightsGameData",
302 | "_init_game_files"
303 | ]
304 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/test/__init__.py
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/test_database_utils.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/test/test_database_utils.py
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/test_models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/test/test_models.py
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/test_open_recruitment.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from nonebug import App
3 | from nonebot.adapters.onebot.v11 import MessageSegment
4 | from .utils import make_fake_message, make_fake_event
5 |
6 |
7 | @pytest.mark.asyncio
8 | async def test_open_recruitment(app: App):
9 | from plugins.nonebot_plugin_arktools.src.tool_open_recruitment import recruit
10 |
11 | Message = make_fake_message()
12 | async with app.test_matcher(recruit) as ctx:
13 | bot = ctx.create_bot()
14 | msg = Message("/公招 高资")
15 | event = make_fake_event(_message=msg)()
16 |
17 | ctx.receive_event(bot, event)
18 | ctx.should_call_send(event, "识别中...", True)
19 |
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/test_operator_info.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/test/test_operator_info.py
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/test_update_utils.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NumberSir/nonebot_plugin_arktools/0eaab75f9bf18dbd84af767668f6ba0416a04214/nonebot_plugin_arktools/test/test_update_utils.py
--------------------------------------------------------------------------------
/nonebot_plugin_arktools/test/utils.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | Fork from: https://github.com/nonebot/nonebot2/blob/master/tests/utils.py
4 | """
5 | from typing import TYPE_CHECKING, Type, Optional
6 |
7 | from pydantic import create_model
8 |
9 | if TYPE_CHECKING:
10 | from nonebot.adapters import Event, Message
11 |
12 |
13 | def make_fake_message() -> Type["Message"]:
14 | from nonebot.adapters import Message, MessageSegment
15 |
16 | class FakeMessageSegment(MessageSegment):
17 | @classmethod
18 | def get_message_class(cls):
19 | return FakeMessage
20 |
21 | def __str__(self) -> str:
22 | return self.data["text"] if self.type == "text" else f"[fake:{self.type}]"
23 |
24 | @classmethod
25 | def text(cls, text: str):
26 | return cls("text", {"text": text})
27 |
28 | @classmethod
29 | def image(cls, url: str):
30 | return cls("image", {"url": url})
31 |
32 | def is_text(self) -> bool:
33 | return self.type == "text"
34 |
35 | class FakeMessage(Message):
36 | @classmethod
37 | def get_segment_class(cls):
38 | return FakeMessageSegment
39 |
40 | @staticmethod
41 | def _construct(msg: str):
42 | yield FakeMessageSegment.text(msg)
43 |
44 | return FakeMessage
45 |
46 |
47 | def make_fake_event(
48 | _type: str = "message",
49 | _name: str = "test",
50 | _description: str = "test",
51 | _user_id: str = "test",
52 | _session_id: str = "test",
53 | _message: Optional["Message"] = None,
54 | _to_me: bool = True,
55 | **fields,
56 | ) -> Type["Event"]:
57 | from nonebot.adapters import Event
58 |
59 | _Fake = create_model("_Fake", __base__=Event, **fields)
60 |
61 | class FakeEvent(_Fake):
62 | def get_type(self) -> str:
63 | return _type
64 |
65 | def get_event_name(self) -> str:
66 | return _name
67 |
68 | def get_event_description(self) -> str:
69 | return _description
70 |
71 | def get_user_id(self) -> str:
72 | return _user_id
73 |
74 | def get_session_id(self) -> str:
75 | return _session_id
76 |
77 | def get_message(self) -> "Message":
78 | if _message is not None:
79 | return _message
80 | raise NotImplementedError
81 |
82 | def is_tome(self) -> bool:
83 | return _to_me
84 |
85 | class Config:
86 | extra = "forbid"
87 |
88 | return FakeEvent
89 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 |
4 | with open("README.md", "r", encoding='utf-8') as f:
5 | long_description = f.read()
6 |
7 |
8 | setuptools.setup(
9 | name="nonebot_plugin_arktools",
10 | version="1.2.0",
11 | author="Number_Sir",
12 | author_email="Number_Sir@126.com",
13 | keywords=["pip", "nonebot2", "nonebot", "nonebot_plugin"],
14 | description="""基于 OneBot 适配器的 NoneBot2 明日方舟小工具箱插件""",
15 | long_description=long_description,
16 | long_description_content_type="text/markdown",
17 | url="https://github.com/NumberSir/nonebot_plugin_arktools",
18 | packages=setuptools.find_packages(),
19 | classifiers=[
20 | "Programming Language :: Python :: 3",
21 | "License :: OSI Approved :: MIT License",
22 | "Operating System :: OS Independent",
23 | ],
24 | include_package_data=True,
25 | platforms="any",
26 | install_requires=[
27 | 'nonebot-adapter-onebot>=2.2.0',
28 | 'nonebot2>=2.0.0rc2',
29 | 'nonebot-plugin-apscheduler>=0.2.0',
30 | 'nonebot-plugin-imageutils>=0.1.14',
31 | 'nonebot-plugin-htmlrender>=0.2.0.1',
32 |
33 | 'httpx>=0.23.1',
34 | 'aiofiles>=0.8.0',
35 | 'tortoise-orm>=0.19.3',
36 | 'lxml>=4.9.2',
37 | 'feedparser>=6.0.10',
38 | ],
39 | python_requires=">=3.8"
40 | )
41 |
--------------------------------------------------------------------------------