├── .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 | nonebot 3 |

4 | 5 |
6 | 7 | # Nonebot_Plugin_ArkTools 8 | 9 | _✨ 基于 OneBot 适配器的 [NoneBot2](https://v2.nonebot.dev/) 明日方舟小工具箱插件 ✨_ 10 | 11 |
12 | 13 | [![OSCS Status](https://www.oscs1024.com/platform/badge/NumberSir/nonebot_plugin_arktools.svg?size=small)](https://www.oscs1024.com/project/NumberSir/nonebot_plugin_arktools?ref=badge_small) [![star](https://gitee.com/Number_Sir/nonebot_plugin_arktools/badge/star.svg?theme=white)](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 | --------------------------------------------------------------------------------