├── .github
├── dependabot.yml
└── workflows
│ ├── pypi-publish.yml
│ └── ruff.yml
├── .gitmodules
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── nonebot-plugin-osubot.iml
└── vcs.xml
├── .pdm-python
├── .pre-commit-config.yaml
├── License
├── README.md
├── nonebot_plugin_osubot
├── __init__.py
├── api.py
├── beatmap_stats_moder.py
├── config.py
├── data
│ └── osu
│ │ ├── 1849145.osz
│ │ └── 1849145.zip
├── database
│ ├── __init__.py
│ └── models.py
├── draw
│ ├── __init__.py
│ ├── bmap.py
│ ├── bp.py
│ ├── catch_preview.py
│ ├── catch_preview_templates
│ │ ├── css
│ │ │ └── style.css
│ │ ├── js
│ │ │ ├── beatmap
│ │ │ │ ├── beatmap.js
│ │ │ │ ├── hitobject.js
│ │ │ │ ├── point.js
│ │ │ │ ├── scroll.js
│ │ │ │ └── timingpoint.js
│ │ │ ├── catch
│ │ │ │ ├── LegacyRandom.js
│ │ │ │ ├── PalpableCatchHitObject.js
│ │ │ │ ├── bananashower.js
│ │ │ │ ├── catch.js
│ │ │ │ ├── fruit.js
│ │ │ │ └── juicestream.js
│ │ │ ├── fakeaudio.js
│ │ │ ├── preview.js
│ │ │ ├── standard
│ │ │ │ ├── curve
│ │ │ │ │ ├── bezier2.js
│ │ │ │ │ ├── catmullcurve.js
│ │ │ │ │ ├── centripetalcatmullrom.js
│ │ │ │ │ ├── circumstancedcircle.js
│ │ │ │ │ ├── curve.js
│ │ │ │ │ ├── curvetype.js
│ │ │ │ │ ├── equaldistancemulticurve.js
│ │ │ │ │ └── linearbezier.js
│ │ │ │ ├── hitcircle.js
│ │ │ │ ├── slider.js
│ │ │ │ └── spinner.js
│ │ │ ├── util.js
│ │ │ ├── viewbox.js
│ │ │ └── zip.min.js
│ │ └── pic.html
│ ├── echarts.py
│ ├── info.py
│ ├── map.py
│ ├── match_history.py
│ ├── rating.py
│ ├── score.py
│ ├── static.py
│ ├── taiko_preview.py
│ ├── templates
│ │ ├── bpa_chart.html
│ │ ├── echarts.min.js
│ │ ├── mod_chart.html
│ │ ├── pp_rank_line_chart.html
│ │ └── t.html
│ └── utils.py
├── exceptions.py
├── file.py
├── info
│ ├── __init__.py
│ ├── bg.py
│ └── bind.py
├── mania
│ └── __init__.py
├── matcher
│ ├── __init__.py
│ ├── bind.py
│ ├── bp.py
│ ├── bp_analyze.py
│ ├── getbg.py
│ ├── guess.py
│ ├── history.py
│ ├── info.py
│ ├── map.py
│ ├── map_convert.py
│ ├── match.py
│ ├── medal.py
│ ├── mu.py
│ ├── osu_help.py
│ ├── osudl.py
│ ├── pr.py
│ ├── preview.py
│ ├── rank.py
│ ├── rating.py
│ ├── recommend.py
│ ├── score.py
│ ├── update.py
│ ├── update_mode.py
│ ├── url_match.py
│ └── utils.py
├── mods.py
├── network
│ ├── __init__.py
│ ├── auto_retry.py
│ ├── first_response.py
│ └── manager.py
├── osufile
│ ├── Best Performance.png
│ ├── History Score.jpg
│ ├── beatmapinfo.png
│ ├── convert.jpg
│ ├── detail.png
│ ├── flags
│ │ ├── AD.png
│ │ ├── AE.png
│ │ ├── AF.png
│ │ ├── AG.png
│ │ ├── AI.png
│ │ ├── AL.png
│ │ ├── AM.png
│ │ ├── AO.png
│ │ ├── AQ.png
│ │ ├── AR.png
│ │ ├── AS.png
│ │ ├── AT.png
│ │ ├── AU.png
│ │ ├── AW.png
│ │ ├── AX.png
│ │ ├── AZ.png
│ │ ├── BA.png
│ │ ├── BB.png
│ │ ├── BD.png
│ │ ├── BE.png
│ │ ├── BF.png
│ │ ├── BG.png
│ │ ├── BH.png
│ │ ├── BI.png
│ │ ├── BJ.png
│ │ ├── BL.png
│ │ ├── BM.png
│ │ ├── BN.png
│ │ ├── BO.png
│ │ ├── BQ (1).png
│ │ ├── BQ (2).png
│ │ ├── BR.png
│ │ ├── BS.png
│ │ ├── BT.png
│ │ ├── BV.png
│ │ ├── BW.png
│ │ ├── BY.png
│ │ ├── BZ.png
│ │ ├── CA.png
│ │ ├── CD.png
│ │ ├── CF.png
│ │ ├── CG.png
│ │ ├── CH.png
│ │ ├── CI.png
│ │ ├── CK.png
│ │ ├── CL.png
│ │ ├── CM.png
│ │ ├── CN.png
│ │ ├── CO.png
│ │ ├── CR.png
│ │ ├── CU.png
│ │ ├── CV.png
│ │ ├── CX.png
│ │ ├── CY.png
│ │ ├── CZ.png
│ │ ├── DE.png
│ │ ├── DJ.png
│ │ ├── DK.png
│ │ ├── DM.png
│ │ ├── DO.png
│ │ ├── DZ.png
│ │ ├── EC.png
│ │ ├── EE.png
│ │ ├── EG.png
│ │ ├── EH.png
│ │ ├── ER.png
│ │ ├── ES.png
│ │ ├── ET.png
│ │ ├── FI.png
│ │ ├── FJ.png
│ │ ├── FK.png
│ │ ├── FO.png
│ │ ├── FR.png
│ │ ├── GA.png
│ │ ├── GB.png
│ │ ├── GD.png
│ │ ├── GE.png
│ │ ├── GF.png
│ │ ├── GG.png
│ │ ├── GH.png
│ │ ├── GI.png
│ │ ├── GL.png
│ │ ├── GM.png
│ │ ├── GN.png
│ │ ├── GP.png
│ │ ├── GQ.png
│ │ ├── GR.png
│ │ ├── GS.png
│ │ ├── GT.png
│ │ ├── GU.png
│ │ ├── GW.png
│ │ ├── GY.png
│ │ ├── HK.png
│ │ ├── HM.png
│ │ ├── HN.png
│ │ ├── HR.png
│ │ ├── HT.png
│ │ ├── HU.png
│ │ ├── ID.png
│ │ ├── IE.png
│ │ ├── IL.png
│ │ ├── IM.png
│ │ ├── IN.png
│ │ ├── IO.png
│ │ ├── IQ.png
│ │ ├── IR.png
│ │ ├── IS.png
│ │ ├── IT.png
│ │ ├── JE.png
│ │ ├── JM.png
│ │ ├── JO.png
│ │ ├── JP.png
│ │ ├── KE.png
│ │ ├── KG.png
│ │ ├── KH.png
│ │ ├── KI.png
│ │ ├── KM.png
│ │ ├── KN.png
│ │ ├── KP.png
│ │ ├── KR.png
│ │ ├── KW.png
│ │ ├── KY.png
│ │ ├── KZ.png
│ │ ├── LA.png
│ │ ├── LB.png
│ │ ├── LC.png
│ │ ├── LI.png
│ │ ├── LK.png
│ │ ├── LR.png
│ │ ├── LS.png
│ │ ├── LT.png
│ │ ├── LU.png
│ │ ├── LV.png
│ │ ├── LY.png
│ │ ├── MA.png
│ │ ├── MC.png
│ │ ├── MD.png
│ │ ├── ME.png
│ │ ├── MF.png
│ │ ├── MG.png
│ │ ├── MH.png
│ │ ├── MK.png
│ │ ├── ML.png
│ │ ├── MM.png
│ │ ├── MN.png
│ │ ├── MO.png
│ │ ├── MP.png
│ │ ├── MQ.png
│ │ ├── MR.png
│ │ ├── MS.png
│ │ ├── MT.png
│ │ ├── MU.png
│ │ ├── MV.png
│ │ ├── MW.png
│ │ ├── MX.png
│ │ ├── MY.png
│ │ ├── MZ.png
│ │ ├── NA.png
│ │ ├── NC.png
│ │ ├── NE.png
│ │ ├── NF.png
│ │ ├── NG.png
│ │ ├── NI.png
│ │ ├── NL.png
│ │ ├── NO.png
│ │ ├── NP.png
│ │ ├── NR.png
│ │ ├── NU.png
│ │ ├── NZ.png
│ │ ├── OM.png
│ │ ├── PA.png
│ │ ├── PE.png
│ │ ├── PF.png
│ │ ├── PG.png
│ │ ├── PH.png
│ │ ├── PK.png
│ │ ├── PL.png
│ │ ├── PM.png
│ │ ├── PN.png
│ │ ├── PR.png
│ │ ├── PS.png
│ │ ├── PT.png
│ │ ├── PW.png
│ │ ├── PY.png
│ │ ├── QA.png
│ │ ├── RE.png
│ │ ├── RO.png
│ │ ├── RS.png
│ │ ├── RU.png
│ │ ├── RW.png
│ │ ├── SA.png
│ │ ├── SB.png
│ │ ├── SC.png
│ │ ├── SD.png
│ │ ├── SE.png
│ │ ├── SG.png
│ │ ├── SH.png
│ │ ├── SI.png
│ │ ├── SJ.png
│ │ ├── SK.png
│ │ ├── SL.png
│ │ ├── SM.png
│ │ ├── SN.png
│ │ ├── SO.png
│ │ ├── SR.png
│ │ ├── SS.png
│ │ ├── ST.png
│ │ ├── SV.png
│ │ ├── SY.png
│ │ ├── SZ.png
│ │ ├── TC.png
│ │ ├── TD.png
│ │ ├── TF.png
│ │ ├── TG.png
│ │ ├── TH.png
│ │ ├── TJ.png
│ │ ├── TK.png
│ │ ├── TL.png
│ │ ├── TM.png
│ │ ├── TN.png
│ │ ├── TO.png
│ │ ├── TR.png
│ │ ├── TT.png
│ │ ├── TV.png
│ │ ├── TW.png
│ │ ├── TZ.png
│ │ ├── UA.png
│ │ ├── UG.png
│ │ ├── UM.png
│ │ ├── US.png
│ │ ├── UY.png
│ │ ├── UZ.png
│ │ ├── VA.png
│ │ ├── VC.png
│ │ ├── VE.png
│ │ ├── VG.png
│ │ ├── VI.png
│ │ ├── VN.png
│ │ ├── VU.png
│ │ ├── WF.png
│ │ ├── WS.png
│ │ ├── YE.png
│ │ ├── YT.png
│ │ ├── ZA.png
│ │ ├── ZM.png
│ │ └── ZW.png
│ ├── fonts
│ │ ├── Extra.otf
│ │ ├── Torus Regular.otf
│ │ ├── Torus SemiBold.otf
│ │ └── Venera.otf
│ ├── help.png
│ ├── info.png
│ ├── maniabeatmapinfo.png
│ ├── match
│ │ ├── mplink.png
│ │ ├── mplink_map.png
│ │ ├── team_blue.png
│ │ └── team_red.png
│ ├── medals
│ │ └── medals.json
│ ├── mods
│ │ ├── 4K.png
│ │ ├── 5K.png
│ │ ├── 6K.png
│ │ ├── 7K.png
│ │ ├── 8K.png
│ │ ├── 9K.png
│ │ ├── AP.png
│ │ ├── CL.png
│ │ ├── DT.png
│ │ ├── EZ.png
│ │ ├── FI.png
│ │ ├── FL.png
│ │ ├── HD.png
│ │ ├── HR.png
│ │ ├── HT.png
│ │ ├── MR.png
│ │ ├── NC.png
│ │ ├── NF.png
│ │ ├── PF.png
│ │ ├── RX.png
│ │ ├── SD.png
│ │ ├── SO.png
│ │ ├── TD.png
│ │ └── V2.png
│ ├── pfm_ctb.png
│ ├── pfm_mania.png
│ ├── pfm_std.png
│ ├── pfm_taiko.png
│ ├── ranking
│ │ ├── ranking-A.png
│ │ ├── ranking-B.png
│ │ ├── ranking-C.png
│ │ ├── ranking-D.png
│ │ ├── ranking-F.png
│ │ ├── ranking-S.png
│ │ ├── ranking-SH.png
│ │ ├── ranking-X.png
│ │ └── ranking-XH.png
│ └── work
│ │ ├── bmap.png
│ │ ├── center.png
│ │ ├── left.png
│ │ ├── right.png
│ │ ├── stardiff.png
│ │ ├── stars.png
│ │ ├── stars_expertplus.png
│ │ └── suppoter.png
├── pp.py
├── schema
│ ├── __init__.py
│ ├── alphaosu.py
│ ├── basemodel.py
│ ├── beatmap.py
│ ├── match.py
│ ├── ppysb
│ │ └── __init__.py
│ ├── sayo_beatmap.py
│ ├── score.py
│ └── user.py
└── utils
│ └── __init__.py
├── pdm.lock
└── pyproject.toml
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/pypi-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 | workflow_dispatch:
8 |
9 | jobs:
10 | pypi-publish:
11 | name: Upload release to PyPI
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@master
15 | - name: Set up Python
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: "3.x"
19 | - name: Install pypa/build
20 | run: >-
21 | python -m
22 | pip install
23 | build
24 | --user
25 | - name: Build a binary wheel and a source tarball
26 | run: >-
27 | python -m
28 | build
29 | --sdist
30 | --wheel
31 | --outdir dist/
32 | .
33 | - name: Publish distribution to PyPI
34 | uses: pypa/gh-action-pypi-publish@release/v1
35 | with:
36 | password: ${{ secrets.PYPI_API_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/ruff.yml:
--------------------------------------------------------------------------------
1 | name: Ruff Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | ruff:
11 | name: Ruff Lint
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Run Ruff Lint
17 | uses: chartboost/ruff-action@v1
18 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "nonebot_plugin_osubot/draw/catch_preview_templates/js"]
2 | path = nonebot_plugin_osubot/draw/catch_preview_templates/js
3 | url = https://github.com/Exsper/osucatch-preview.git
4 | branch = master
5 | sparse-checkout = docs/jsgit submodule update --init --recursive
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/nonebot-plugin-osubot.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.pdm-python:
--------------------------------------------------------------------------------
1 | C:/Users/57247/PycharmProjects/nonebot-plugin-osubot/.venv/Scripts/python.exe
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_install_hook_types: [pre-commit, prepare-commit-msg]
2 | ci:
3 | autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
4 | autofix_prs: true
5 | autoupdate_branch: master
6 | autoupdate_schedule: monthly
7 | autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
8 | repos:
9 | - repo: https://github.com/astral-sh/ruff-pre-commit
10 | rev: v0.11.8
11 | hooks:
12 | - id: ruff
13 | args: [--fix]
14 | stages: [pre-commit]
15 | - id: ruff-format
16 | stages: [pre-commit]
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |

5 |
6 |
7 |
8 |
9 | # nonebot-plugin-osubot
10 |
11 | _✨ NoneBot osubot ✨_
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |

21 |
22 |
23 |
24 |
25 | ## 📖 介绍
26 |
27 | 本项目修改自[osuv2](https://github.com/Yuri-YuzuChaN/osuv2),适配了nonebot2,并且在此之上修改了命令的响应逻辑并修改了一些bug使之更易于使用
28 |
29 | 变速功能依赖ffmpeg,需要[自行安装ffmpeg](https://docs.go-cqhttp.org/guide/quick_start.html#%E5%AE%89%E8%A3%85-ffmpeg)才能正常使用
30 |
31 | ## 💿 安装
32 |
33 |
34 | 使用 nb-cli 安装(推荐)
35 | 在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
36 |
37 | nb plugin install nonebot-plugin-osubot
38 |
39 |
40 |
41 |
42 | 使用包管理器安装(推荐)
43 | 在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
44 |
45 |
46 | pip
47 |
48 | pip install nonebot-plugin-osubot
49 |
50 |
51 | pdm
52 |
53 | pdm add nonebot-plugin-osubot
54 |
55 |
56 | poetry
57 |
58 | poetry add nonebot-plugin-osubot
59 |
60 |
61 |
62 | 打开 nonebot2 项目的 `bot.py` 文件, 在其中写入
63 |
64 | nonebot.load_plugin('nonebot_plugin_osubot')
65 |
66 |
67 |
68 |
69 | 从 github 安装
70 | 在 nonebot2 项目的插件目录下, 打开命令行, 输入以下命令克隆此储存库
71 |
72 | git clone https://github.com/yaowan233/nonebot-plugin-osubot.git
73 |
74 | 修改[nonebot_plugin_osubot的__init__.py](https://github.com/yaowan233/nonebot-plugin-osubot/blob/a7c7098f39d92b8fe74dfe85c262397b81db721c/nonebot_plugin_osubot/__init__.py#L37)为
75 |
76 | add_model('src.plugins.database.models')
77 |
78 | 打开 nonebot2 项目的 `bot.py` 文件, 在其中写入
79 |
80 | nonebot.load_plugin('src.plugins.nonebot_plugin_osubot')
81 |
82 |
83 |
84 | ## ⚙️ 配置
85 | 你需要至[OSU个人设置](https://osu.ppy.sh/home/account/edit)申请新的OAuth应用,然后将得到的客户端ID与客户端密钥填入nonebot2 项目的`.env`文件中
86 |
87 | 配置说明
88 | | 配置项 | 必填 | 默认值 | 说明 |
89 | |:-----:|:----:|:----:|:----:|
90 | | OSU_CLIENT | 是 | 无 | 客户端ID |
91 | | OSU_KEY | 是 | 无 | 客户端密钥 |
92 | | tortoise_orm_db_url | 否 | sqlite://db.sqlite3 | 数据库地址 |
93 | | INFO_BG | 否 | ['https://example.com'] | 随机背景api地址,需要打开网页后随机获得一张图片 |
94 |
95 | ## 🎉 使用
96 | ### 指令
97 |
98 | 
99 |
100 |
101 | ## 💡 贡献
102 |
103 | 如果遇到任何问题,欢迎提各种issue来反馈bug
104 | 你也可以加群(228986744)来进行反馈!
105 | 
106 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/__init__.py:
--------------------------------------------------------------------------------
1 | from nonebot import require
2 | from nonebot.log import logger
3 | from nonebot.plugin import PluginMetadata, inherit_supported_adapters
4 |
5 | require("nonebot_plugin_apscheduler")
6 | require("nonebot_plugin_alconna")
7 | require("nonebot_plugin_session")
8 | require("nonebot_plugin_tortoise_orm")
9 | require("nonebot_plugin_htmlrender")
10 | require("nonebot_plugin_waiter")
11 | from nonebot_plugin_apscheduler import scheduler
12 | from nonebot_plugin_tortoise_orm import add_model
13 |
14 | from .config import Config
15 | from .matcher import * # noqa
16 | from .info import update_users_info
17 | from .database.models import UserData
18 |
19 | add_model("nonebot_plugin_osubot.database.models")
20 | usage = "发送/osuhelp 查看帮助"
21 | __plugin_meta__ = PluginMetadata(
22 | name="OSUBot",
23 | description="OSU查分插件",
24 | usage=usage,
25 | type="application",
26 | homepage="https://github.com/yaowan233/nonebot-plugin-osubot",
27 | config=Config,
28 | supported_adapters=inherit_supported_adapters("nonebot_plugin_session", "nonebot_plugin_alconna"),
29 | extra={
30 | "unique_name": "osubot",
31 | "author": "yaowan233 <572473053@qq.com>",
32 | },
33 | )
34 |
35 |
36 | @scheduler.scheduled_job("cron", hour="0", misfire_grace_time=60)
37 | async def update_info():
38 | result = await UserData.all()
39 | if not result:
40 | return
41 | users = [i.osu_id for i in result]
42 | groups = [users[i : i + 50] for i in range(0, len(users), 50)]
43 | for group in groups:
44 | await update_users_info(group)
45 | logger.info(f"已更新{len(result)}位玩家数据")
46 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/beatmap_stats_moder.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from .utils import GM
4 | from .schema import Beatmap
5 | from .schema.score import Mod, UnifiedScore
6 |
7 | OD0_MS = 80
8 | OD10_MS = 20
9 | AR0_MS = 1800
10 | AR5_MS = 1200
11 | AR10_MS = 450
12 |
13 | OD_MS_STEP = (OD0_MS - OD10_MS) / 10
14 | AR_MS_STEP1 = (AR0_MS - AR5_MS) / 5
15 | AR_MS_STEP2 = (AR5_MS - AR10_MS) / 5
16 |
17 |
18 | def modify_ar(base_ar, speed_mul, multiplier):
19 | ar = base_ar
20 | ar *= multiplier
21 |
22 | arms = AR0_MS - AR_MS_STEP1 * ar if ar < 5 else AR5_MS - AR_MS_STEP2 * (ar - 5)
23 |
24 | arms = min(AR0_MS, max(AR10_MS, arms))
25 | arms /= speed_mul
26 |
27 | ar = (AR0_MS - arms) / AR_MS_STEP1 if arms > AR5_MS else 5 + (AR5_MS - arms) / AR_MS_STEP2
28 | return ar
29 |
30 |
31 | def modify_od(base_od, speed_mul, multiplier):
32 | od = base_od
33 | od *= multiplier
34 | odms = OD0_MS - OD_MS_STEP * od
35 | odms = min(OD0_MS, max(OD10_MS, odms))
36 | odms /= speed_mul
37 | od = (OD0_MS - odms) / OD_MS_STEP
38 | return od
39 |
40 |
41 | def with_mods(mapinfo: Beatmap, scoreinfo: Optional[UnifiedScore], mods: list[Mod]):
42 | speed_mul = 1
43 | od_ar_hp_multiplier = 1
44 | mode = GM[scoreinfo.ruleset_id] if scoreinfo else mapinfo.mode
45 | for mod in mods:
46 | if mod.acronym == "DA" and mod.settings:
47 | if mod.settings.get("circle_size"):
48 | mapinfo.cs = mod.settings["circle_size"]
49 | if mod.settings.get("approach_rate"):
50 | mapinfo.ar = mod.settings["approach_rate"]
51 | if mod.settings.get("drain_rate"):
52 | mapinfo.drain = mod.settings["drain_rate"]
53 | if mod.settings.get("overall_difficulty"):
54 | mapinfo.accuracy = mod.settings["overall_difficulty"]
55 | if mod.acronym == "DT" or mod.acronym == "NC":
56 | speed_mul = 1.5
57 | if mod.settings and mod.settings.get("speed_change"):
58 | speed_mul = mod.settings["speed_change"]
59 | mapinfo.bpm *= speed_mul
60 | mapinfo.total_length /= speed_mul
61 | if mod.acronym == "HT":
62 | speed_mul = 0.75
63 | if mod.settings and mod.settings.get("speed_change"):
64 | speed_mul = mod.settings["speed_change"]
65 | mapinfo.bpm *= speed_mul
66 | mapinfo.total_length /= speed_mul
67 | if mod.acronym == "HR":
68 | od_ar_hp_multiplier = 1.4
69 | if mod.acronym == "EZ":
70 | od_ar_hp_multiplier *= 0.5
71 | if mode == "mania":
72 | speed_mul = 1
73 | if mode not in ("mania", "taiko"):
74 | mapinfo.ar = modify_ar(mapinfo.ar, speed_mul, od_ar_hp_multiplier)
75 | if mode == "fruits":
76 | speed_mul = 1
77 | mapinfo.accuracy = modify_od(mapinfo.accuracy, speed_mul, od_ar_hp_multiplier)
78 | if mode not in ("mania", "taiko"):
79 | if Mod(acronym="HR") in mods:
80 | mapinfo.cs *= 1.3
81 | if Mod(acronym="EZ") in mods:
82 | mapinfo.cs *= 0.5
83 | mapinfo.cs = min(10.0, mapinfo.cs)
84 | mapinfo.drain *= od_ar_hp_multiplier
85 | mapinfo.drain = min(10.0, mapinfo.drain)
86 | return mapinfo
87 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/config.py:
--------------------------------------------------------------------------------
1 | from typing import Union, Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Config(BaseModel):
7 | osu_client: Optional[int] = None
8 | osu_key: Optional[str] = None
9 | info_bg: Optional[list[str]] = ["https://t.alcy.cc/mp", "https://t.alcy.cc/moemp"]
10 | osu_proxy: Optional[Union[str, dict]] = None
11 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/data/osu/1849145.osz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaowan233/nonebot-plugin-osubot/bc65bc8109156c8aac09a0516f3f7b191f2f8e90/nonebot_plugin_osubot/data/osu/1849145.osz
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/data/osu/1849145.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaowan233/nonebot-plugin-osubot/bc65bc8109156c8aac09a0516f3f7b191f2f8e90/nonebot_plugin_osubot/data/osu/1849145.zip
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/database/__init__.py:
--------------------------------------------------------------------------------
1 | from .models import InfoData, UserData, SbUserData
2 |
3 | __all__ = ["UserData", "InfoData", "SbUserData"]
4 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/database/models.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from tortoise import Model, fields
4 |
5 |
6 | class UserData(Model):
7 | id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
8 | """自增主键"""
9 | user_id: str = fields.TextField()
10 | """用户id"""
11 | osu_id: int = fields.IntField()
12 | """osu id"""
13 | osu_name: str = fields.TextField()
14 | """osu 用户名"""
15 | osu_mode: int = fields.IntField()
16 | """osu 模式"""
17 | lazer_mode: bool = fields.BooleanField(default=False, null=True)
18 | """是否启用lazer模式"""
19 |
20 | class Meta:
21 | table = "User"
22 | indexes = ("user_id",)
23 |
24 |
25 | class InfoData(Model):
26 | id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
27 | """自增主键"""
28 | osu_id: int = fields.IntField()
29 | """osu id"""
30 | c_rank: Optional[int] = fields.IntField(null=True)
31 | """国家排名"""
32 | g_rank: Optional[int] = fields.IntField(null=True)
33 | """世界排名"""
34 | pp: float = fields.FloatField()
35 | """pp"""
36 | acc: float = fields.FloatField()
37 | """acc"""
38 | pc: int = fields.IntField()
39 | """游戏次数"""
40 | count: int = fields.IntField()
41 | """打击note数"""
42 | osu_mode: int = fields.IntField()
43 | """osu 模式"""
44 | date = fields.DateField()
45 |
46 | class Meta:
47 | table = "Info"
48 | indexes = ("id",)
49 |
50 |
51 | class SbUserData(Model):
52 | id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
53 | """自增主键"""
54 | user_id: str = fields.TextField()
55 | """用户id"""
56 | osu_id: int = fields.IntField()
57 | """osu id"""
58 | osu_name: str = fields.TextField()
59 | """osu 用户名"""
60 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/__init__.py:
--------------------------------------------------------------------------------
1 | from .bp import draw_bp
2 | from .info import draw_info
3 | from .map import draw_map_info
4 | from .bmap import draw_bmap_info
5 | from .score import draw_score, get_score_data
6 |
7 | __all__ = ["draw_info", "draw_score", "draw_bp", "draw_map_info", "draw_bmap_info", "get_score_data"]
8 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import jinja2
4 | from nonebot_plugin_htmlrender import get_new_page
5 |
6 | from ..file import map_path, download_osu
7 |
8 | template_path = str(Path(__file__).parent / "catch_preview_templates")
9 |
10 |
11 | async def draw_cath_preview(beatmap_id, beatmapset_id, mods) -> bytes:
12 | path = map_path / str(beatmapset_id)
13 | if not path.exists():
14 | path.mkdir(parents=True, exist_ok=True)
15 | osu = path / f"{beatmap_id}.osu"
16 | if not osu.exists():
17 | await download_osu(beatmapset_id, beatmap_id)
18 | with open(osu, encoding="utf-8-sig") as f:
19 | osu_file = f.read()
20 | template_name = "pic.html"
21 | template_env = jinja2.Environment( # noqa: S701
22 | loader=jinja2.FileSystemLoader(template_path),
23 | enable_async=True,
24 | )
25 | template = template_env.get_template(template_name)
26 | is_hr = 1 if "HR" in mods else 0
27 | is_ez = 1 if "EZ" in mods else 0
28 | is_dt = 1 if "DT" in mods else 0
29 | is_ht = 1 if "HT" in mods else 0
30 | async with get_new_page(2) as page:
31 | await page.goto(f"file://{template_path}")
32 | await page.set_content(
33 | await template.render_async(
34 | beatmap_id=beatmap_id, osu_file=osu_file, is_hr=is_hr, is_ez=is_ez, is_dt=is_dt, is_ht=is_ht
35 | ),
36 | wait_until="networkidle",
37 | )
38 | return await page.screenshot(full_page=True, type="jpeg", quality=60, omit_background=True)
39 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/beatmap/hitobject.js:
--------------------------------------------------------------------------------
1 | function HitObject(data, beatmap)
2 | {
3 | this.beatmap = beatmap;
4 |
5 | this.position = new Point(data);
6 | this.endPosition = this.position.clone();
7 | this.time = data[2] | 0;
8 | this.endTime = this.time;
9 | this.flag = data[3] | 0;
10 | this.hitSound = data[4] | 0;
11 | }
12 | HitObject.prototype.draw = undefined;
13 | HitObject.parse = function(line, beatmap)
14 | {
15 | var data = line.split(',');
16 | if (data.length < 5)
17 | {
18 | throw 'invalid data: ' + line;
19 | }
20 |
21 | var type = data[3] & beatmap.hitObjectTypeMask;
22 | if (!(type in beatmap.hitObjectTypes))
23 | {
24 | // throw 'we do not support this hitobject type';
25 | return new HitObject(data, beatmap);
26 | }
27 |
28 | return new beatmap.hitObjectTypes[type](data, beatmap);
29 | };
30 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/beatmap/point.js:
--------------------------------------------------------------------------------
1 | function Point()
2 | {
3 | if (arguments.length >= 2)
4 | {
5 | this.x = +arguments[0];
6 | this.y = +arguments[1];
7 | }
8 | else if (arguments.length == 1)
9 | {
10 | this.x = +arguments[0][0];
11 | this.y = +arguments[0][1];
12 | }
13 | else
14 | {
15 | this.x = 0;
16 | this.y = 0;
17 | }
18 | }
19 | Point.prototype.distanceTo = function(point)
20 | {
21 | return Math.hypot(point.x - this.x, point.y - this.y);
22 | };
23 | Point.prototype.equalTo = function(point)
24 | {
25 | return point instanceof Point &&
26 | this.x == point.x && this.y == point.y;
27 | };
28 | Point.prototype.angleTo = function(point)
29 | {
30 | return Math.atan2(point.y - this.y, point.x - this.x);
31 | };
32 | Point.prototype.clone = function()
33 | {
34 | return new Point(this.x, this.y);
35 | };
36 | Point.prototype.translate = function(n)
37 | {
38 | if (n instanceof Point)
39 | {
40 | this.x += n.x;
41 | this.y += n.y;
42 | }
43 | else
44 | {
45 | this.x += n;
46 | this.y += n;
47 | }
48 | return this;
49 | };
50 | Point.prototype.scale = function(n)
51 | {
52 | this.x *= n;
53 | this.y *= n;
54 | return this;
55 | };
56 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/beatmap/scroll.js:
--------------------------------------------------------------------------------
1 | function Scroll(osu)
2 | {
3 | Beatmap.call(this, osu);
4 |
5 | // dp for numerous call to this.scrollAt
6 | this.scrollAtTimingPointIndex = [ 0 ];
7 | var currentIdx = this.timingPointIndexAt(0),
8 | current = this.TimingPoints[currentIdx],
9 | base = this.TimingPoints[0],
10 | scrollVelocity = base.beatLength / current.beatLength;
11 | this.scrollAtTimingPointIndex[currentIdx] = current.time * scrollVelocity;
12 | while (++currentIdx < this.TimingPoints.length)
13 | {
14 | var next = this.TimingPoints[currentIdx];
15 | this.scrollAtTimingPointIndex[currentIdx] = (next.time - current.time) * scrollVelocity +
16 | this.scrollAtTimingPointIndex[currentIdx - 1];
17 | current = next;
18 | scrollVelocity = base.beatLength / current.beatLength;
19 | }
20 |
21 | this.barLines = [];
22 | var endTime = (this.HitObjects.length ? this.HitObjects[this.HitObjects.length - 1].endTime : 0) + 1;
23 | for (var i = 0; i < this.TimingPoints.length; i++)
24 | {
25 | var current = this.TimingPoints[i],
26 | base = current.parent || current,
27 | barLength = base.beatLength * base.meter,
28 | next = this.TimingPoints[i + 1],
29 | barLineLimit = next ? (next.parent || next).time : endTime;
30 | for (var barTime = base.time; barTime < barLineLimit; barTime += barLength)
31 | {
32 | this.barLines.push(this.scrollAt(barTime));
33 | }
34 | }
35 | }
36 | Scroll.prototype = Object.create(Beatmap.prototype);
37 | Scroll.prototype.constructor = Scroll;
38 | Scroll.prototype.scrollAt = function(time)
39 | {
40 | var currentIdx = this.timingPointIndexAt(time),
41 | current = this.TimingPoints[currentIdx],
42 | base = this.TimingPoints[0],
43 | scrollVelocity = base.beatLength / current.beatLength;
44 | return (time - current.time) * scrollVelocity + this.scrollAtTimingPointIndex[currentIdx];
45 | };
46 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/beatmap/timingpoint.js:
--------------------------------------------------------------------------------
1 | function TimingPoint(line)
2 | {
3 | var data = line.split(',');
4 | if (data.length < 2)
5 | {
6 | throw 'invalid data';
7 | }
8 |
9 | this.time = data[0] | 0;
10 | this.beatLength = +data[1];
11 | this.meter = (data[2] | 0) || 4;
12 | this.kiai = (data[7] | 0) % 2;
13 |
14 | // this is non-inherited timingPoint
15 | if (this.beatLength >= 0)
16 | {
17 | TimingPoint.parent = this;
18 | this.sliderVelocity = 1;
19 | }
20 | else
21 | {
22 | this.parent = TimingPoint.parent;
23 | this.sliderVelocity = -100 / this.beatLength;
24 | this.beatLength = this.parent.beatLength / this.sliderVelocity;
25 | this.meter = this.parent.meter;
26 | }
27 | }
28 | Object.defineProperties(TimingPoint.prototype, {
29 | bpm: {
30 | get: function()
31 | {
32 | return 60000 / this.beatLength;
33 | }
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/catch/LegacyRandom.js:
--------------------------------------------------------------------------------
1 | class LegacyRandom{
2 | constructor(x) {
3 | this.int_to_real = 1 / 2147483648;
4 | this.int_mask = 0x7FFFFFFF;
5 | this.x = x || Date.now();
6 | this.x = this.ToUInt(this.x);
7 | this._X = this.x;
8 | this.y = 842502087;
9 | this._Y = this.y;
10 | this.z = 3579807591;
11 | this._Z = this.z;
12 | this.w = 273326509;
13 | this._W = this.w;
14 |
15 | this.bitBuffer;
16 | this.bitIndex = 32;
17 | }
18 |
19 | ToInt(num) {
20 | if (num > 2147483647) return this.ToInt(num - 4294967296);
21 | else if (num < -2147483648) return this.ToInt(4294967296 + num);
22 | else return parseInt(num);
23 | }
24 |
25 | ToUInt(num) {
26 | if (num > 4294967296) return this.ToUInt(num - 4294967296);
27 | if (num < 0) return this.ToUInt(4294967296 + num);
28 | else return num;
29 | }
30 |
31 | NextUInt() {
32 | let t = this.ToUInt(this._X ^ this.ToUInt(this._X << 11));
33 | this._X = this._Y;
34 | this._Y = this._Z;
35 | this._Z = this._W;
36 | let tmp = this.ToUInt(this._W >>> 19);
37 | tmp = this.ToUInt(this._W ^ tmp);
38 | tmp = this.ToUInt(tmp ^ t);
39 | let tmp2 = this.ToUInt(t >>> 8);
40 | this._W = this.ToUInt(tmp ^ tmp2);
41 | return this._W;
42 | }
43 |
44 | Next() {
45 | if (arguments.length <= 0) {
46 | return this.ToInt(this.int_mask & this.NextUInt());
47 | }
48 | else if (arguments.length === 1) { // upperBound
49 | return this.ToInt(this.NextDouble() * arguments[0]);
50 | }
51 | else { // lowerBound, upperBound
52 | return this.ToInt(arguments[0] + this.NextDouble() * (arguments[1] - arguments[0]));
53 | }
54 | }
55 |
56 | NextDouble() {
57 | return this.int_to_real * this.Next();
58 | }
59 |
60 | NextBool() {
61 | if (this.bitIndex == 32)
62 | {
63 | this.bitBuffer = this.NextUInt();
64 | this.bitIndex = 1;
65 |
66 | return ((this.bitBuffer & 1) == 1);
67 | }
68 |
69 | this.bitIndex++;
70 | this.bitBuffer = this.bitBuffer >>> 1;
71 | return ((this.bitBuffer & 1) == 1);
72 | }
73 | }
74 | /*
75 | let rng = new LegacyRandom(1337);
76 | for(let i = 0; i < 1000; i++)
77 | {
78 | rng.NextDouble();
79 | }
80 | console.log(rng.NextDouble());
81 | */
82 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/catch/bananashower.js:
--------------------------------------------------------------------------------
1 | function BananaShower(data, beatmap) {
2 | Spinner.call(this, data, beatmap);
3 |
4 | this.endTime = data[5] | 0;
5 | this.duration = this.endTime - this.time;
6 |
7 | this.nested = [];
8 | }
9 | BananaShower.prototype = Object.create(Spinner.prototype);
10 | BananaShower.prototype.constructor = BananaShower;
11 | BananaShower.ID = 8;
12 | Catch.prototype.hitObjectTypes[BananaShower.ID] = BananaShower;
13 | BananaShower.prototype.buildNested = function() {
14 | this.nested = [];
15 | let spacing = this.duration;
16 | while (spacing > 100) spacing /= 2;
17 | if (spacing <= 0) return;
18 | let time = this.time;
19 | let i = 0;
20 | while (time <= this.endTime) {
21 | this.nested.push(new PalpableCatchHitObject({
22 | type: "Banana",
23 | time,
24 | x: 0,
25 | color: 'rgb(255,240,0)',
26 | radius: this.beatmap.bananaRadius,
27 | }, this.beatmap));
28 |
29 | time += spacing;
30 | i++;
31 | }
32 | this.nested[this.nested.length - 1].hitSound = this.hitSound;
33 | return this;
34 | };
35 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/catch/fruit.js:
--------------------------------------------------------------------------------
1 | function Fruit(data, beatmap)
2 | {
3 | HitObject.call(this, data, beatmap);
4 | }
5 | Fruit.prototype = Object.create(HitObject.prototype, {
6 | newCombo: {
7 | get: function()
8 | {
9 | return this.flag & 4;
10 | }
11 | },
12 | comboSkip: {
13 | get: function()
14 | {
15 | return this.flag >> 4;
16 | }
17 | }
18 | });
19 | Fruit.prototype.constructor = Fruit;
20 | Fruit.ID = 1;
21 | Catch.prototype.hitObjectTypes[Fruit.ID] = Fruit;
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/fakeaudio.js:
--------------------------------------------------------------------------------
1 | function FakeAudio(length) {
2 | this.src = null;
3 | this.duration = length;
4 | this.length = length; // seconds
5 | this.currentTime = 0; // seconds
6 | this.volume = 1;
7 | this.paused = true;
8 | this.playbackRate = 1;
9 | this.playTimer = null;
10 | this.pause();
11 | }
12 |
13 | FakeAudio.prototype.draw = function () {
14 | requestAnimationFrame(() => {
15 | window.preview.at(this.currentTime * 1000);
16 | });
17 | }
18 |
19 | FakeAudio.prototype.play = function () {
20 | preview.beatmap.refresh();
21 | this.draw();
22 | let ts = 20;
23 | if (this.paused) {
24 | $('#play').removeClass('e');
25 | this.paused = false;
26 | this.playTimer = setInterval(()=> {
27 | this.currentTime += ts / 1000 * this.playbackRate;
28 | if (this.currentTime >= this.length) this.pause();
29 | if (this.currentTime % 1 < 0.1) {
30 | $('#progress').val(this.currentTime);
31 | $('#playtime').text(time2text(this.currentTime));
32 | }
33 | this.draw();
34 | }, ts);
35 | }
36 | };
37 |
38 | FakeAudio.prototype.pause = function () {
39 | if (this.paused) return;
40 | $(document.body).trigger('mousemove');
41 | $('#play').addClass('e');
42 | this.paused = true;
43 | clearInterval(this.playTimer);
44 | };
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/preview.js:
--------------------------------------------------------------------------------
1 | var bgblob;
2 | function Preview(dest) {
3 | this.container = dest;
4 |
5 | this.screen = document.createElement('canvas');
6 | this.screen.width = Beatmap.WIDTH;
7 | this.screen.height = Beatmap.HEIGHT;
8 | this.ctx = this.screen.getContext('2d');
9 | this.container.appendChild(this.screen);
10 |
11 | var self = this;
12 | this.background = new Image();
13 | this.background.setAttribute('crossOrigin', 'anonymous');
14 | let drawBG = function () {
15 | var canvas = document.createElement('canvas');
16 | canvas.id = 'bgcanvas';
17 | canvas.width = self.screen.width;
18 | canvas.height = self.screen.height;
19 | var ctx = canvas.getContext('2d');
20 |
21 | // background-size: cover height
22 | var sWidth = this.height * (self.screen.width / self.screen.height);
23 | ctx.drawImage(this, (this.width - sWidth) / 2, 0, sWidth, this.height,
24 | 0, 0, self.screen.width, self.screen.height);
25 | // background dim
26 | ctx.fillStyle = 'rgba(0, 0, 0, .5)';
27 | ctx.fillRect(0, 0, self.screen.width, self.screen.height);
28 |
29 | if (typeof self.beatmap.processBG != 'undefined') {
30 | self.beatmap.processBG(ctx);
31 | }
32 | canvas.toBlob(function (blob) {
33 | var url = URL.createObjectURL(blob);
34 | self.background.src = url;
35 | self.container.style.backgroundImage = 'url(' + url + ')';
36 | });
37 | }
38 | this.background.addEventListener('load', drawBG, { once: true });
39 | this.background.addEventListener('error', function () {
40 | self.container.style.backgroundImage = 'none';
41 | });
42 | }
43 | Preview.prototype.load = function (bgblob, osufile, mod, success, fail) {
44 | if (typeof this.xhr != 'undefined') {
45 | this.xhr.abort();
46 | }
47 |
48 | let mods = {
49 | HR: false,
50 | EZ: false
51 | }
52 | if (mod) mods[mod] = true;
53 |
54 | var self = this;
55 | try {
56 | self.beatmap = Beatmap.parse(osufile, mods);
57 | if (bgblob) self.background.src = bgblob;
58 | self.ctx.restore();
59 | self.ctx.save();
60 | self.beatmap.update(self.ctx);
61 | self.at(0);
62 |
63 | if (typeof success == 'function') {
64 | success.call(self);
65 | }
66 | }
67 | catch (e) {
68 | if (typeof fail == 'function') {
69 | fail.call(self, e);
70 | }
71 | }
72 | };
73 | Preview.prototype.at = function (time) {
74 | this.ctx.save();
75 | this.ctx.setTransform(1, 0, 0, 1, 0, 0);
76 | this.ctx.clearRect(0, 0, Beatmap.WIDTH, Beatmap.HEIGHT);
77 | this.ctx.restore();
78 | this.beatmap.draw(time, this.ctx);
79 | };
80 |
81 | Preview.prototype.output = function () {
82 | let SCALE = 0.2;
83 | let canvas2 = this.beatmap.draw2(SCALE);
84 | canvas2.toBlob(function (blob) {
85 | // 创建下载链接
86 | let link = document.createElement("a");
87 | link.href = URL.createObjectURL(blob);
88 | link.download = "预览图.png";
89 |
90 | // 触发下载
91 | link.click();
92 | }, "image/png");
93 | };
94 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/bezier2.js:
--------------------------------------------------------------------------------
1 | function Bezier2(points)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/Bezier2.java
4 | if (points.length < 2)
5 | {
6 | throw 'invalid data';
7 | }
8 |
9 | this.points = points;
10 | var approxLength = 0;
11 | for (var i = 1; i < this.points.length; i++)
12 | {
13 | approxLength += this.points[i].distanceTo(this.points[i - 1]);
14 | }
15 |
16 | CurveType.call(this, approxLength);
17 | }
18 | Bezier2.prototype = Object.create(CurveType.prototype);
19 | Bezier2.prototype.constructor = Bezier2;
20 | Bezier2.prototype.pointAt = function(t)
21 | {
22 | var n = this.points.length - 1,
23 | point = new Point(),
24 | combination = 1;
25 | for (var i = 0; i <= n; i++)
26 | {
27 | var bernstein = combination * Math.pow(t, i) * Math.pow(1 - t, n - i);
28 | point.x += this.points[i].x * bernstein;
29 | point.y += this.points[i].y * bernstein;
30 | combination = combination * (n - i) / (i + 1);
31 | }
32 | return point;
33 | };
34 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/catmullcurve.js:
--------------------------------------------------------------------------------
1 | function CatmullCurve(points, pixelLength)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/CatmullCurve.java
4 | var catmulls = [],
5 | controls = [];
6 | if (!points[0].equalTo(points[1]))
7 | {
8 | controls.push(points[0]);
9 | }
10 | for (var i = 0; i < points.length; i++)
11 | {
12 | controls.push(points[i]);
13 | try
14 | {
15 | catmulls.push(new CentripetalCatmullRom(controls));
16 | controls.shift();
17 | }
18 | catch (e) {}
19 | }
20 | var point2 = points.slice(-2);
21 | if (!point2[1].equalTo(point2[0]))
22 | {
23 | controls.push(point2[1]);
24 | }
25 | try
26 | {
27 | catmulls.push(new CentripetalCatmullRom(controls));
28 | }
29 | catch (e) {}
30 |
31 | EqualDistanceMultiCurve.call(this, catmulls, pixelLength);
32 | };
33 | CatmullCurve.prototype = Object.create(EqualDistanceMultiCurve.prototype);
34 | CatmullCurve.prototype.constructor = CatmullCurve;
35 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/centripetalcatmullrom.js:
--------------------------------------------------------------------------------
1 | function CentripetalCatmullRom(points)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/CentripetalCatmullRom.java
4 | // needs 4 points
5 | if (points.length != 4)
6 | {
7 | throw 'invalid data';
8 | }
9 |
10 | this.points = points;
11 | var approxLength = 0;
12 | for (var i = 1; i < 4; i++)
13 | {
14 | approxLength += this.points[i].distanceTo(this.points[i - 1]);
15 | }
16 |
17 | CurveType.call(this, approxLength / 2);
18 | }
19 | CentripetalCatmullRom.prototype = Object.create(CurveType.prototype);
20 | CentripetalCatmullRom.prototype.constructor = CentripetalCatmullRom;
21 | CentripetalCatmullRom.prototype.pointAt = function(t)
22 | {
23 | t = Math.lerp(1, 2, t);
24 | var A1 = this.points[0].clone().scale(1 - t).translate(this.points[1].clone().scale(t));
25 | var A2 = this.points[1].clone().scale(2 - t).translate(this.points[2].clone().scale(t - 1));
26 | var A3 = this.points[2].clone().scale(3 - t).translate(this.points[3].clone().scale(t - 2));
27 | var B1 = A1.clone().scale(2 - t).translate(A2.clone().scale(t));
28 | var B2 = A2.clone().scale(3 - t).translate(A3.clone().scale(t - 1));
29 | return B1.clone().scale(2 - t).translate(B2.clone().scale(t - 1)).scale(0.5);
30 | };
31 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/circumstancedcircle.js:
--------------------------------------------------------------------------------
1 | function CircumscribedCircle(points, pixelLength)
2 | {
3 | var a = points[0].x - points[1].x, b = points[0].y - points[1].y,
4 | c = points[1].x - points[2].x, d = points[1].y - points[2].y,
5 | q = (a * d - b * c) * 2,
6 | l0 = points[0].x * points[0].x + points[0].y * points[0].y,
7 | l1 = points[1].x * points[1].x + points[1].y * points[1].y,
8 | l2 = points[2].x * points[2].x + points[2].y * points[2].y,
9 | x = ((l0 - l1) * d + (l1 - l2) * -b) / q,
10 | y = ((l0 - l1) * -c + (l1 - l2) * a) / q,
11 | dx = points[0].x - x,
12 | dy = points[0].y - y,
13 | r = Math.hypot(dx, dy),
14 | base = Math.atan2(dy, dx),
15 | t = pixelLength / r * Math.ccw(points[0], points[1], points[2]);
16 | if (!t)
17 | {
18 | // when points[2] is missing or vectors are parallel
19 | throw 'invalid data';
20 | }
21 | this.circle = {
22 | x: x,
23 | y: y,
24 | radius: r
25 | };
26 | this.angle = {
27 | base: base,
28 | delta: t
29 | };
30 |
31 | var nCurve = pixelLength / Curve.PRECISION | 0;
32 | this.path = [];
33 | for (var i = 0; i <= nCurve; i++)
34 | {
35 | this.path[i] = this.pointAt(i / nCurve);
36 | }
37 |
38 | Curve.call(this);
39 | };
40 | CircumscribedCircle.prototype = Object.create(Curve.prototype);
41 | CircumscribedCircle.prototype.constructor = CircumscribedCircle;
42 | CircumscribedCircle.prototype.pointAt = function(t)
43 | {
44 | var angle = this.angle.base + this.angle.delta * t;
45 | return new Point(this.circle.x + Math.cos(angle) * this.circle.radius,
46 | this.circle.y + Math.sin(angle) * this.circle.radius);
47 | };
48 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/curve.js:
--------------------------------------------------------------------------------
1 | function Curve()
2 | {
3 | this.startAngle = this.path[0].angleTo(this.path[1]);
4 | var path2 = this.path.slice(-2);
5 | this.endAngle = path2[1].angleTo(path2[0]);
6 | }
7 | Curve.prototype.path = undefined;
8 | Curve.prototype.pointAt = undefined;
9 | Curve.PRECISION = 5;
10 | Curve.parse = function(sliderType, points, pixelLength)
11 | {
12 | try
13 | {
14 | if (sliderType == 'P')
15 | {
16 | return new CircumscribedCircle(points, pixelLength);
17 | }
18 | if (sliderType == 'C')
19 | {
20 | return new CatmullCurve(points, pixelLength);
21 | }
22 | }
23 | catch(e) {}
24 | try
25 | {
26 | return new LinearBezier(points, pixelLength, sliderType == 'L');
27 | }
28 | catch(e)
29 | {
30 | return new SingleNoteCurve(points[0]);
31 | }
32 | }
33 |
34 |
35 |
36 | function SingleNoteCurve(point)
37 | {
38 | this.path = [point];
39 | };
40 | SingleNoteCurve.prototype = Object.create(Curve.prototype);
41 | SingleNoteCurve.prototype.constructor = SingleNoteCurve;
42 | SingleNoteCurve.prototype.pointAt = function(t)
43 | {
44 | return this.path[0];
45 | };
46 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/curvetype.js:
--------------------------------------------------------------------------------
1 | function CurveType(approxLength)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/CurveType.java
4 | var points = (approxLength / 4 | 0) + 1;
5 | this.path = [];
6 | for (var i = 0; i <= points; i++)
7 | {
8 | this.path[i] = this.pointAt(i / points);
9 | }
10 |
11 | this.distance = [ 0 ];
12 | for (var i = 1; i <= points; i++)
13 | {
14 | this.distance[i] = this.path[i].distanceTo(this.path[i - 1]);
15 | }
16 | }
17 | CurveType.prototype.pointAt = undefined;
18 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/equaldistancemulticurve.js:
--------------------------------------------------------------------------------
1 | function EqualDistanceMultiCurve(curves, pixelLength)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/EqualDistanceMultiCurve.java
4 | var nCurve = pixelLength / Curve.PRECISION | 0;
5 | this.path = [];
6 |
7 | var distanceAt = 0,
8 | curveIndex = 0,
9 | curve = curves[curveIndex],
10 | pointIndex = 0,
11 | startPoint = curve.path[0],
12 | lastDistanceAt = 0;
13 | // for each distance, try to get in between the two points that are between it
14 | for (var i = 0; i <= nCurve; i++)
15 | {
16 | var prefDistance = i * pixelLength / nCurve | 0;
17 | while (distanceAt < prefDistance)
18 | {
19 | lastDistanceAt = distanceAt;
20 | startPoint = curve.path[pointIndex];
21 |
22 | if (++pointIndex >= curve.path.length)
23 | {
24 | if (curveIndex + 1 < curves.length)
25 | {
26 | curve = curves[++curveIndex];
27 | pointIndex = 0;
28 | }
29 | else
30 | {
31 | pointIndex = curve.path.length - 1;
32 | if (lastDistanceAt == distanceAt)
33 | {
34 | // out of points even though the preferred distance hasn't been reached
35 | break;
36 | }
37 | }
38 | }
39 | distanceAt += curve.distance[pointIndex];
40 | }
41 | var endPoint = curve.path[pointIndex];
42 |
43 | // interpolate the point between the two closest distances
44 | if (distanceAt - lastDistanceAt > 1)
45 | {
46 | this.path[i] = Math.lerp(startPoint, endPoint, (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt));
47 | }
48 | else
49 | {
50 | this.path[i] = endPoint;
51 | }
52 | }
53 |
54 | Curve.call(this);
55 | };
56 | EqualDistanceMultiCurve.prototype = Object.create(Curve.prototype);
57 | EqualDistanceMultiCurve.prototype.constructor = EqualDistanceMultiCurve;
58 | EqualDistanceMultiCurve.prototype.pointAt = function(t)
59 | {
60 | var indexF = this.path.length * t,
61 | index = indexF | 0;
62 | if (index + 1 < this.path.length)
63 | {
64 | return Math.lerp(this.path[index], this.path[index + 1], indexF - index);
65 | }
66 | else
67 | {
68 | return this.path[this.path.length - 1];
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/curve/linearbezier.js:
--------------------------------------------------------------------------------
1 | function LinearBezier(points, pixelLength, linear)
2 | {
3 | // https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/curves/LinearBezier.java
4 | var beziers = [],
5 | controls = [],
6 | last;
7 | for (var i = 0; i < points.length; i++)
8 | {
9 | var point = points[i];
10 | if (linear)
11 | {
12 | if (typeof last != 'undefined')
13 | {
14 | controls.push(point);
15 | beziers.push(new Bezier2(controls));
16 | controls = [];
17 | }
18 | }
19 | else if (point.equalTo(last))
20 | {
21 | try
22 | {
23 | beziers.push(new Bezier2(controls));
24 | }
25 | catch (e) {}
26 | controls = [];
27 | }
28 | controls.push(point);
29 | last = point;
30 | }
31 | try
32 | {
33 | beziers.push(new Bezier2(controls));
34 | }
35 | catch (e) {}
36 |
37 | EqualDistanceMultiCurve.call(this, beziers, pixelLength);
38 | };
39 | LinearBezier.prototype = Object.create(EqualDistanceMultiCurve.prototype);
40 | LinearBezier.prototype.constructor = LinearBezier;
41 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/hitcircle.js:
--------------------------------------------------------------------------------
1 | function HitCircle(data, beatmap)
2 | {
3 | HitObject.call(this, data, beatmap);
4 |
5 | this.stack = 0;
6 | }
7 | HitCircle.prototype = Object.create(HitObject.prototype, {
8 | newCombo: {
9 | get: function()
10 | {
11 | return this.flag & 4;
12 | }
13 | },
14 | comboSkip: {
15 | get: function()
16 | {
17 | return this.flag >> 4;
18 | }
19 | }
20 | });
21 | HitCircle.prototype.constructor = HitCircle;
22 | HitCircle.ID = 1;
23 | HitCircle.FADE_IN_TIME = 375;
24 | HitCircle.FADE_OUT_TIME = 200;
25 | HitCircle.prototype.draw = function(time, ctx)
26 | {
27 | var dt = this.time - time,
28 | opacity = 1;
29 | if (dt >= 0)
30 | {
31 | opacity = (this.beatmap.approachTime - dt) / HitCircle.FADE_IN_TIME;
32 | }
33 | else
34 | {
35 | opacity = 1 + dt / HitCircle.FADE_OUT_TIME;
36 | }
37 | ctx.globalAlpha = Math.max(0, Math.min(opacity, 1));
38 |
39 | this.drawCircle(this.position, ctx);
40 | this.drawText(this.position, this.combo, 0, ctx);
41 | if (dt >= 0)
42 | {
43 | this.drawApproach(dt, ctx);
44 | }
45 | };
46 | HitCircle.prototype.drawCircle = function(position, ctx)
47 | {
48 | // HitCircle
49 | ctx.beginPath();
50 | ctx.arc(position.x - this.stack * this.beatmap.stackOffset,
51 | position.y - this.stack * this.beatmap.stackOffset,
52 | this.beatmap.circleRadius - this.beatmap.circleBorder / 2, -Math.PI, Math.PI);
53 | ctx.shadowBlur = 0;
54 | ctx.fillStyle = this.color;
55 | ctx.fill();
56 | // Overlay
57 | ctx.shadowBlur = this.beatmap.shadowBlur;
58 | ctx.strokeStyle = '#fff';
59 | ctx.lineWidth = this.beatmap.circleBorder;
60 | ctx.stroke();
61 | };
62 | HitCircle.prototype.drawText = function(position, text, deg, ctx)
63 | {
64 | ctx.shadowBlur = this.beatmap.shadowBlur;
65 | ctx.fillStyle = '#fff';
66 | ctx.save();
67 | ctx.translate(position.x - this.stack * this.beatmap.stackOffset,
68 | position.y - this.stack * this.beatmap.stackOffset);
69 | ctx.rotate(deg);
70 | ctx.fillText(text, 0, 0);
71 | ctx.restore();
72 | };
73 | HitCircle.prototype.drawApproach = function(dt, ctx)
74 | {
75 | var scale = 1 + dt / this.beatmap.approachTime * 3;
76 | ctx.beginPath();
77 | ctx.arc(this.position.x - this.stack * this.beatmap.stackOffset,
78 | this.position.y - this.stack * this.beatmap.stackOffset,
79 | this.beatmap.circleRadius * scale - this.beatmap.circleBorder / 2, -Math.PI, Math.PI);
80 | ctx.shadowBlur = 0;
81 | ctx.strokeStyle = this.color;
82 | ctx.lineWidth = this.beatmap.circleBorder / 2 * scale;
83 | ctx.stroke();
84 | };
85 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/standard/spinner.js:
--------------------------------------------------------------------------------
1 | function Spinner(data, beatmap)
2 | {
3 | HitCircle.call(this, data, beatmap);
4 |
5 | this.endTime = data[5] | 0;
6 | this.duration = this.endTime - this.time;
7 | }
8 | Spinner.prototype = Object.create(HitCircle.prototype);
9 | Spinner.prototype.constructor = Spinner;
10 | Spinner.ID = 8;
11 | Spinner.FADE_IN_TIME = 500;
12 | Spinner.FADE_OUT_TIME = 200;
13 | Spinner.RADIUS = Beatmap.MAX_Y / 2;
14 | Spinner.BORDER_WIDTH = Spinner.RADIUS / 20;
15 | Spinner.prototype.draw = function(time, ctx)
16 | {
17 | var dt = this.time - time,
18 | opacity = 1;
19 | if (dt >= 0)
20 | {
21 | opacity = (this.beatmap.approachTime - dt) / Spinner.FADE_IN_TIME;
22 | }
23 | else if (time > this.endTime)
24 | {
25 | opacity = 1 - (time - this.endTime) / Spinner.FADE_OUT_TIME;
26 | }
27 | ctx.globalAlpha = Math.max(0, Math.min(opacity, 1));
28 | ctx.save();
29 | // Spinner
30 | ctx.beginPath();
31 | ctx.arc(this.position.x, this.position.y,
32 | Spinner.RADIUS - Spinner.BORDER_WIDTH / 2, -Math.PI, Math.PI);
33 | ctx.globalCompositeOperation = 'destination-over';
34 | ctx.shadowBlur = 0;
35 | ctx.fillStyle = 'rgba(0,0,0,.4)';
36 | ctx.fill();
37 | // Border
38 | ctx.shadowBlur = Spinner.BORDER_WIDTH;
39 | ctx.strokeStyle = '#fff';
40 | ctx.lineWidth = Spinner.BORDER_WIDTH;
41 | ctx.stroke();
42 | ctx.restore();
43 | // Approach
44 | if (dt < 0 && time <= this.endTime)
45 | {
46 | var scale = 1 + dt / this.duration;
47 | ctx.beginPath();
48 | ctx.arc(this.position.x, this.position.y,
49 | (Spinner.RADIUS - Spinner.BORDER_WIDTH / 2) * scale, -Math.PI, Math.PI);
50 | ctx.shadowBlur = 3;
51 | ctx.strokeStyle = '#fff';
52 | ctx.lineWidth = (Spinner.BORDER_WIDTH / 2) * scale;
53 | ctx.stroke();
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/util.js:
--------------------------------------------------------------------------------
1 | Math.hypot = Math.hypot || function()
2 | {
3 | var power = 0;
4 | for (var i = 0; i < arguments.length; i++)
5 | {
6 | power += arguments[i] * arguments[i];
7 | }
8 | return Math.sqrt(power);
9 | };
10 | Math.lerp = Math.lerp || function(a, b, t)
11 | {
12 | if (a instanceof Point)
13 | {
14 | return new Point(Math.lerp(a.x, b.x, t), Math.lerp(a.y, b.y, t));
15 | }
16 | return a + (b - a) * t;
17 | };
18 | /** Calculate point C's direction by line AB
19 | * @return {Number}
20 | * 1 C is counter-clockwise to AB
21 | * 0 C is parallel to AB
22 | * -1 C is clockwise to AB
23 | */
24 | Math.ccw = function(a, b, c)
25 | {
26 | var ret = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
27 | if (ret > 0)
28 | {
29 | return 1;
30 | }
31 | if (ret < 0)
32 | {
33 | return -1;
34 | }
35 | return 0;
36 | };
37 | Math.clamp = function(num, min, max) {
38 | if (num < min) return min;
39 | if (num > max) return max;
40 | return num;
41 | }
42 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
43 | if (!HTMLCanvasElement.prototype.toBlob)
44 | {
45 | Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
46 | value: function (callback, type, quality)
47 | {
48 |
49 | var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
50 | len = binStr.length,
51 | arr = new Uint8Array(len);
52 |
53 | for (var i = 0; i < len; i++)
54 | {
55 | arr[i] = binStr.charCodeAt(i);
56 | }
57 |
58 | callback(new Blob([ arr ], { type: type || 'image/png' }));
59 | }
60 | });
61 | }
62 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/js/viewbox.js:
--------------------------------------------------------------------------------
1 | /** viewbox stretches child to fit parent proportionally.
2 | *
3 | * viewbox must have a child that has two attributes, 'width' and 'height'.
4 | */
5 | document.addEventListener('DOMContentLoaded', function()
6 | {
7 | window.addEventListener('resize', thunk);
8 | thunk();
9 |
10 | function thunk()
11 | {
12 | var viewboxes = document.getElementsByClassName('x-viewbox');
13 | for (var i = 0; i < viewboxes.length; i++)
14 | {
15 | onresize.call(viewboxes[i]);
16 | }
17 | }
18 |
19 | function onresize()
20 | {
21 | var parent = this.parentElement,
22 | child = this.firstChild,
23 | w, h, cw, ch, sw, sh, s;
24 |
25 | if (!parent)
26 | {
27 | w = window.innerWidth;
28 | h = window.innerHeight;
29 | }
30 | else
31 | {
32 | w = parent.scrollWidth;
33 | h = parent.scrollHeight;
34 | }
35 |
36 | if (child)
37 | {
38 | child.style.transformOrigin = '0 0';
39 |
40 | cw = child.width;
41 | ch = child.height;
42 | }
43 | sw = w / cw;
44 | sh = h / ch;
45 | // if viewbox has no child that has proper attributes,
46 | // ignore size of child and re-calculate scale.
47 | if ((sw || sh) == NaN)
48 | {
49 | sw = w;
50 | sh = h;
51 | }
52 |
53 | if (sw > sh)
54 | {
55 | this.style.width = sh * cw + 'px';
56 | this.style.height = h + 'px';
57 | s = sh;
58 | }
59 | else
60 | {
61 | this.style.width = w + 'px';
62 | this.style.height = sw * ch + 'px';
63 | s = sw;
64 | }
65 | if (child)
66 | {
67 | child.style.transform = 'scale(' + s + ')';
68 | }
69 | }
70 | });
71 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/catch_preview_templates/pic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ctb图片预览 网址后加#bid(-EZ/HR)
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/echarts.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from nonebot_plugin_htmlrender import template_to_pic
4 |
5 | template_path = str(Path(__file__).parent / "templates")
6 |
7 |
8 | async def draw_history_plot(pp_ls, date_ls, rank_ls, title) -> bytes:
9 | template_name = "pp_rank_line_chart.html"
10 | pic = await template_to_pic(
11 | template_path,
12 | template_name,
13 | {"pp_ls": pp_ls, "date_ls": date_ls, "rank_ls": rank_ls, "title": title},
14 | )
15 | return pic
16 |
17 |
18 | async def draw_bpa_plot(name, pp_ls, length_ls, mod_pp_ls, mapper_pp_ls) -> bytes:
19 | template_name = "bpa_chart.html"
20 | pic = await template_to_pic(
21 | template_path,
22 | template_name,
23 | {"name": name, "pp_ls": pp_ls, "length_ls": length_ls, "mod_pp_ls": mod_pp_ls, "mapper_pp_ls": mapper_pp_ls},
24 | )
25 | return pic
26 |
27 |
28 | # async def draw_strains_plot(time, strains) -> bytes:
29 | # template_name = "basic_line_chart.html"
30 | # pic = await template_to_pic(
31 | # template_path,
32 | # template_name,
33 | # {"time": time, "strains": strains},
34 | # )
35 | # return pic
36 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/match_history.py:
--------------------------------------------------------------------------------
1 | from playwright.async_api import ViewportSize
2 | from nonebot_plugin_htmlrender import get_new_page
3 |
4 |
5 | async def draw_match_history(match_id: str) -> bytes:
6 | async with get_new_page(
7 | viewport=ViewportSize(width=900, height=1), extra_http_headers={"Accept-Language": "zh-CN"}
8 | ) as page:
9 | await page.goto(f"https://osu.ppy.sh/community/matches/{match_id}", wait_until="networkidle")
10 | await page.add_style_tag(
11 | content="""
12 | .mp-history-event, .mp-history-content__item--event, .nav2, .audio-player {
13 | display: none !important;
14 | }
15 | """
16 | )
17 | while True:
18 | try:
19 | await page.wait_for_selector(".show-more-link", timeout=3000)
20 | await page.evaluate("document.querySelector('.show-more-link').click()")
21 | except Exception:
22 | break
23 | pic = await page.screenshot(omit_background=True, full_page=True, quality=60, type="jpeg")
24 | return pic
25 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/templates/mod_chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Awesome-pyecharts
4 |
5 |
6 |
7 |
8 |
61 |
62 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/draw/templates/pp_rank_line_chart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Awesome-pyecharts
4 |
5 |
6 |
7 |
8 |
58 |
59 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/exceptions.py:
--------------------------------------------------------------------------------
1 | class NetworkError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/info/__init__.py:
--------------------------------------------------------------------------------
1 | from .bg import get_bg
2 | from .bind import bind_user_info, update_users_info
3 |
4 | __all__ = ["get_bg", "bind_user_info", "update_users_info"]
5 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/info/bg.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import datetime
3 | from io import BytesIO
4 | from pathlib import Path
5 | from typing import Union
6 |
7 | from PIL import Image, UnidentifiedImageError
8 |
9 | from ..api import osu_api, get_map_bg
10 | from ..exceptions import NetworkError
11 | from ..file import re_map, map_path, download_osu
12 |
13 |
14 | async def get_bg(mapid: Union[str, int], setid: int = None) -> BytesIO:
15 | if not setid:
16 | info = await osu_api("map", map_id=mapid)
17 | setid: int = info["beatmapset_id"]
18 | set_path = map_path / str(setid)
19 | if not set_path.exists():
20 | set_path.mkdir(parents=True, exist_ok=True)
21 | osu = map_path / str(setid) / f"{mapid}.osu"
22 | if not osu.exists():
23 | await download_osu(setid, mapid)
24 | cover = re_map(osu)
25 | cover_path = map_path / str(setid) / cover
26 | if not cover_path.exists():
27 | if bg := await get_map_bg(mapid, setid, cover):
28 | with open(cover_path, "wb") as f:
29 | f.write(bg.getvalue())
30 | try:
31 | img = Image.open(cover_path).convert("RGBA")
32 | except UnidentifiedImageError:
33 | cover_path.unlink()
34 | raise NetworkError("暂时无法下载背景图片>︿<")
35 | byt = BytesIO()
36 | img.save(byt, "png")
37 | _ = asyncio.create_task(update_bg(cover_path))
38 | return byt
39 |
40 |
41 | async def update_bg(cover_path: Path):
42 | creation_time = cover_path.stat().st_ctime
43 | creation_datetime = datetime.datetime.fromtimestamp(creation_time)
44 | time_diff = datetime.datetime.now() - creation_datetime
45 | if time_diff > datetime.timedelta(days=1):
46 | cover_path.unlink()
47 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/__init__.py:
--------------------------------------------------------------------------------
1 | from .mu import mu
2 | from .info import info
3 | from .bp import bp, tbp
4 | from .getbg import getbg
5 | from .match import match
6 | from .medal import medal
7 | from .score import score
8 | from .pr import pr, recent
9 | from .rating import rating
10 | from .history import history
11 | from .bind import bind, unbind
12 | from .map import bmap, osu_map
13 | from .osu_help import osu_help
14 | from .rank import group_pp_rank
15 | from .recommend import recommend
16 | from .url_match import url_match
17 | from .bp_analyze import bp_analyze
18 | from .update_mode import update_mode
19 | from .preview import generate_preview
20 | from .map_convert import change, convert, generate_full_ln
21 | from .update import update_pic, update_info, clear_background
22 | from .guess import hint, pic_hint, guess_pic, guess_audio, word_matcher, pic_word_matcher
23 |
24 | __all__ = [
25 | "guess_audio",
26 | "guess_pic",
27 | "word_matcher",
28 | "pic_word_matcher",
29 | "hint",
30 | "pic_hint",
31 | "medal",
32 | "bp_analyze",
33 | "pr",
34 | "recent",
35 | "osu_help",
36 | "url_match",
37 | "recommend",
38 | "update_info",
39 | "update_pic",
40 | "clear_background",
41 | "generate_preview",
42 | "getbg",
43 | "bind",
44 | "unbind",
45 | "bp",
46 | "tbp",
47 | "info",
48 | "osu_map",
49 | "bmap",
50 | "mu",
51 | "score",
52 | "update_mode",
53 | "history",
54 | "convert",
55 | "change",
56 | "generate_full_ln",
57 | "match",
58 | "rating",
59 | "group_pp_rank",
60 | ]
61 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/bind.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from nonebot import on_command
4 | from nonebot.params import CommandArg
5 | from nonebot_plugin_alconna import UniMessage
6 | from nonebot.internal.adapter import Event, Message
7 |
8 | from ..api import get_uid_by_name
9 | from ..info import bind_user_info
10 | from ..exceptions import NetworkError
11 | from ..database import UserData, SbUserData
12 |
13 | bind = on_command("bind", priority=11, block=True)
14 | unbind = on_command("unbind", priority=11, block=True)
15 | sbbind = on_command("sbbind", priority=11, block=True)
16 | sbunbind = on_command("sbunbind", priority=11, block=True)
17 | lock = asyncio.Lock()
18 |
19 |
20 | @bind.handle()
21 | async def _bind(event: Event, name: Message = CommandArg()):
22 | name = name.extract_plain_text().strip()
23 | if not name:
24 | await UniMessage.text("请在指令后输入您的 osuid").finish(reply_to=True)
25 | async with lock:
26 | if user := await UserData.get_or_none(user_id=event.get_user_id()):
27 | await UniMessage.text(f"您已绑定{user.osu_name},如需要解绑请输入/unbind").finish(reply_to=True)
28 | try:
29 | msg = await bind_user_info("bind", name, event.get_user_id())
30 | except NetworkError:
31 | await UniMessage.text(f"绑定失败,找不到叫 {name} 的人哦").finish(reply_to=True)
32 | await UniMessage.text(msg).finish(reply_to=True)
33 |
34 |
35 | @sbbind.handle()
36 | async def _(event: Event, name: Message = CommandArg()):
37 | name = name.extract_plain_text().strip()
38 | if not name:
39 | await UniMessage.text("请在指令后输入您的 osuid").finish(reply_to=True)
40 | async with lock:
41 | if user := await SbUserData.get_or_none(user_id=event.get_user_id()):
42 | await UniMessage.text(f"您已绑定{user.osu_name},如需要解绑请输入/sbunbind").finish(reply_to=True)
43 | try:
44 | uid = await get_uid_by_name(name, "ppysb")
45 | except NetworkError:
46 | await UniMessage.text(f"绑定失败,找不到叫 {name} 的人哦").finish(reply_to=True)
47 | await SbUserData.create(user_id=event.get_user_id(), osu_id=uid, osu_name=name)
48 | await UniMessage.text(f"成功绑定 ppysb 服务器用户: {name}").finish(reply_to=True)
49 |
50 |
51 | @unbind.handle()
52 | async def _unbind(event: Event):
53 | if _ := await UserData.get_or_none(user_id=event.get_user_id()):
54 | await UserData.filter(user_id=event.get_user_id()).delete()
55 | await UniMessage.text("解绑成功!").send(reply_to=True)
56 | else:
57 | await UniMessage.text("尚未绑定,无需解绑").send(reply_to=True)
58 |
59 |
60 | @sbunbind.handle()
61 | async def _(event: Event):
62 | if _ := await SbUserData.get_or_none(user_id=event.get_user_id()):
63 | await SbUserData.filter(user_id=event.get_user_id()).delete()
64 | await UniMessage.text("解绑成功!").send(reply_to=True)
65 | else:
66 | await UniMessage.text("尚未绑定,无需解绑").send(reply_to=True)
67 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/getbg.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_command
2 | from nonebot.params import CommandArg
3 | from nonebot.internal.adapter import Message
4 | from nonebot_plugin_alconna import UniMessage
5 |
6 | from ..info import get_bg
7 | from ..exceptions import NetworkError
8 |
9 | getbg = on_command("getbg", priority=11, block=True)
10 |
11 |
12 | @getbg.handle()
13 | async def _get_bg(bg: Message = CommandArg()):
14 | bg = bg.extract_plain_text().strip()
15 | if not bg:
16 | await UniMessage.text("请输入需要提取BG的地图ID").finish(reply_to=True)
17 | try:
18 | byt = await get_bg(bg)
19 | except NetworkError as e:
20 | await UniMessage.text(f"获取图片时 {str(e)}").finish(reply_to=True)
21 | await UniMessage.image(raw=byt).finish(reply_to=True)
22 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/history.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from nonebot import on_command
4 | from nonebot.typing import T_State
5 | from nonebot_plugin_alconna import UniMessage
6 |
7 | from ..utils import NGM
8 | from .utils import split_msg
9 | from ..database import InfoData, UserData
10 | from ..draw.echarts import draw_history_plot
11 |
12 | history = on_command("history", priority=11, block=True)
13 |
14 |
15 | @history.handle(parameterless=[split_msg()])
16 | async def _info(state: T_State):
17 | if "error" in state:
18 | await UniMessage.text(state["error"]).finish(reply_to=True)
19 | data = InfoData.filter(osu_id=state["user"], osu_mode=state["mode"])
20 | user = await UserData.filter(osu_id=state["user"]).first()
21 | if not user:
22 | await UniMessage.text(f"没有{state['user']}的数据哦").finish(reply_to=True)
23 | if state["day"] > 0:
24 | data = data.filter(date__gte=datetime.date.today() - datetime.timedelta(days=state["day"]))
25 | data = await data.order_by("date").all()
26 | pp_ls = [i.pp for i in data]
27 | date_ls = [str(i.date) for i in data]
28 | rank_ls = [i.g_rank for i in data]
29 | # 使用列表推导式筛选出 rank_ls 不为 None 的索引
30 | filtered_indices = [index for index, rank in enumerate(rank_ls) if rank is not None and rank != 0]
31 |
32 | # 根据筛选出的索引生成新的列表
33 | pp_ls = [pp_ls[i] for i in filtered_indices]
34 | date_ls = [date_ls[i] for i in filtered_indices]
35 | rank_ls = [rank_ls[i] for i in filtered_indices]
36 | byt = await draw_history_plot(pp_ls, date_ls, rank_ls, f"{user.osu_name} {NGM[state['mode']]} pp/rank history")
37 | await UniMessage.image(raw=byt).finish(reply_to=True)
38 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/info.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_command
2 | from nonebot.typing import T_State
3 | from nonebot_plugin_alconna import UniMessage
4 |
5 | from ..utils import NGM
6 | from ..draw import draw_info
7 | from .utils import split_msg
8 | from ..exceptions import NetworkError
9 |
10 | info = on_command("info", priority=11, block=True, aliases={"Info", "INFO"})
11 |
12 |
13 | @info.handle(parameterless=[split_msg()])
14 | async def _info(state: T_State):
15 | if "error" in state:
16 | await UniMessage.text(state["error"]).finish(reply_to=True)
17 | try:
18 | data = await draw_info(state["user"], NGM[state["mode"]], state["day"], state["source"])
19 | except NetworkError as e:
20 | await UniMessage.text(
21 | f"在查找用户:{state['username']} {NGM[state['mode']]}模式 {state['day']}日内 成绩时{str(e)}"
22 | ).finish(reply_to=True)
23 | await UniMessage.image(raw=data).finish(reply_to=True)
24 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/map.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_command
2 | from nonebot.typing import T_State
3 | from nonebot_plugin_alconna import UniMessage
4 |
5 | from .utils import split_msg
6 | from ..exceptions import NetworkError
7 | from ..draw import draw_map_info, draw_bmap_info
8 |
9 | osu_map = on_command("map", priority=11, block=True)
10 | bmap = on_command("bmap", priority=11, block=True)
11 |
12 |
13 | @osu_map.handle(parameterless=[split_msg()])
14 | async def _map(state: T_State):
15 | map_id = state["target"]
16 | mods = state["mods"]
17 | if not map_id:
18 | await UniMessage.text("请输入地图ID").finish(reply_to=True)
19 | try:
20 | m = await draw_map_info(map_id, mods, state["is_lazer"])
21 | except NetworkError as e:
22 | mods = f" mod:{state['mods']}" if state["mods"] else ""
23 | await UniMessage.text(f"在查找地图mapid:{state['target']}{mods}时 {str(e)}").finish(reply_to=True)
24 | await UniMessage.image(raw=m).finish(reply_to=True)
25 |
26 |
27 | @bmap.handle(parameterless=[split_msg()])
28 | async def _bmap(state: T_State):
29 | set_id = state["target"]
30 | if not set_id:
31 | await UniMessage.text("请输入setID").finish(reply_to=True)
32 | try:
33 | m = await draw_bmap_info(set_id)
34 | except NetworkError as e:
35 | mods = f" mod:{state['mods']}" if state["mods"] else ""
36 | await UniMessage.text(f"在查找地图setid:{state['target']}{mods}时 {str(e)}").finish(reply_to=True)
37 | await UniMessage.image(raw=m).finish(reply_to=True)
38 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/match.py:
--------------------------------------------------------------------------------
1 | from nonebot import on_command
2 | from nonebot.params import CommandArg
3 | from nonebot.internal.adapter import Message
4 | from nonebot_plugin_alconna import UniMessage
5 |
6 | from ..draw.match_history import draw_match_history
7 |
8 | match = on_command("match", priority=11, block=True)
9 |
10 |
11 | @match.handle()
12 | async def _help(arg: Message = CommandArg()):
13 | arg = arg.extract_plain_text().strip()
14 | img = await draw_match_history(arg)
15 | await UniMessage.image(raw=img).finish(reply_to=True)
16 |
--------------------------------------------------------------------------------
/nonebot_plugin_osubot/matcher/medal.py:
--------------------------------------------------------------------------------
1 | import re
2 | import json
3 | from pathlib import Path
4 |
5 | from nonebot import on_command
6 | from nonebot.params import CommandArg
7 | from nonebot.internal.adapter import Message
8 | from nonebot_plugin_alconna import UniMessage
9 |
10 | from ..api import safe_async_get
11 |
12 | medal_data_path = Path(__file__).parent.parent / "osufile" / "medals" / "medals.json"
13 | with open(medal_data_path) as file:
14 | medal_json = json.load(file)
15 |
16 |
17 | medal = on_command("medal", aliases={"成就"}, priority=11, block=True)
18 |
19 |
20 | @medal.handle()
21 | async def _(msg: Message = CommandArg()):
22 | name = msg.extract_plain_text()
23 | data = await safe_async_get(f"https://osekai.net/medals/api/public/get_medal.php?medal={name}")
24 | medal_data = data.json()
25 | if "MedalID" not in medal_data:
26 | await medal.finish("没有找到欸,看看是不是名字打错了")
27 | words = ""
28 | if medal_data["Restriction"] != "NULL":
29 | words += f"限制模式:{medal_data['Restriction']}\n"
30 | words += "获得方式:\n"
31 | if medal_data["Name"] in medal_json:
32 | words += medal_json[medal_data["Name"]]["MedalSolution"]
33 | else:
34 | words += medal_data["Solution"] if medal_data["Solution"] else medal_data["Instructions"]
35 | table_regex = r"]*>(.*?)<\/table>"
36 | table_match = re.search(table_regex, words, re.DOTALL)
37 | if table_match:
38 | table_text = table_match.group(1)
39 |
40 | # 使用正则表达式匹配并提取表格行部分
41 | row_regex = r"]*>(.*?)<\/tr>"
42 | rows = re.findall(row_regex, table_text, re.DOTALL)
43 |
44 | result = ""
45 |
46 | # 遍历表格行并提取单元格内容
47 | for row in rows:
48 | # 使用正则表达式匹配并提取单元格部分
49 | cell_regex = r"]*>(.*?)<\/t[hd]>"
50 | cells = re.findall(cell_regex, row, re.DOTALL)
51 | for cell in cells:
52 | # 去除单元格内的HTML标签
53 | cell_text = re.sub(r"<[^>]*>", "", cell)
54 | result += cell_text + " "
55 | result += "\n"
56 |
57 | # 将提取的表格文字替换回原文中
58 | words = re.sub(table_regex, result, words)
59 | style_regex = r"