44 Commits

Author SHA1 Message Date
MaxOhn e6ed5a770a release: v1.1.0 2024-07-12 13:58:51 +02:00
MaxOhn ecec26fda5 updated changelog 2024-07-12 13:51:04 +02:00
Badewanne3 bf6e57a520 feat: use rosu-mods (#9)
* use rosu-mods

* mention mods in readme

* use published rosu-pp

* remove test remnants
2024-07-12 13:42:24 +02:00
MaxOhn 4d29dd752b add python 3.13 to ci 2024-07-08 13:05:45 +02:00
Jeglerjeg 5ed4ce3414 dep: Update to py03 0.22 (#8) 2024-07-04 22:16:02 +02:00
MaxOhn fc60284511 v1.0.1 2024-05-05 21:03:20 +02:00
MaxOhn b94dbf06d7 updated changelog 2024-05-05 21:01:59 +02:00
Badewanne3 0c2aa11ccd fix PerformanceAttributes.pp_accuracy field name (#7) 2024-05-05 20:57:12 +02:00
MaxOhn 4a8b0a9a1b require clippy to pass when releasing 2024-04-03 15:35:29 +02:00
MaxOhn ab03b988e2 updated hyperlinks in readme 2024-04-03 15:31:04 +02:00
MaxOhn f0f08fe9c9 rename section_length 2024-04-03 15:27:01 +02:00
MaxOhn cb4b965eae update hyperlinks in readme 2024-04-03 15:27:00 +02:00
MaxOhn 3c7f6533fe update changelog 2024-04-03 15:27:00 +02:00
MaxOhn e2fb4cc342 specify MSRV for toolchain in ci 2024-04-03 15:27:00 +02:00
MaxOhn c5fdf695d7 use sccache 2024-04-03 15:27:00 +02:00
MaxOhn b1fe7f33b2 adjust bool formatting 2024-04-03 15:27:00 +02:00
MaxOhn bcad53579a minor adjustments 2024-04-03 15:26:59 +02:00
MaxOhn 5d68891f66 remove requirements install 2024-04-03 15:26:59 +02:00
MaxOhn fa981c4769 adjusted pyproject.toml 2024-04-03 15:26:59 +02:00
MaxOhn 0bbd18648e use published rosu-pp 2024-04-03 15:26:59 +02:00
MaxOhn 197d687b98 bump pyo3 from 0.20 to 0.21 2024-04-03 15:26:59 +02:00
MaxOhn 0ce07c0e6c remove mode kwarg from Beatmap constructor 2024-04-03 15:26:59 +02:00
MaxOhn 757b807f54 updated readme hyperlinks 2024-04-03 15:26:59 +02:00
MaxOhn f7c41a516f updated readme 2024-04-03 15:26:59 +02:00
MaxOhn c0e1977a54 api adjustments 2024-04-03 15:26:59 +02:00
MaxOhn 2a37169ca8 slight ci simplification 2024-04-03 15:26:59 +02:00
MaxOhn 583ce7ef0e fix yml syntax 2024-04-03 15:26:59 +02:00
MaxOhn 361002989f updated ci 2024-04-03 15:26:57 +02:00
MaxOhn a8e521ef8c rewrite 2024-04-03 15:25:39 +02:00
MaxOhn 4fd090d625 Merge branch 'next' 2023-02-09 08:21:34 +01:00
MaxOhn ce30124c5e v0.9.4 2023-02-09 08:21:28 +01:00
MaxOhn 45e27f71e9 updated rosu-pp 2023-02-09 08:20:10 +01:00
Max 84a37b904e specify python version 2023-01-28 21:47:06 +01:00
Max bffc06c17e remove some requirements 2023-01-28 21:43:20 +01:00
Max 4fe3395457 downgrade ubuntu in linux job 2023-01-28 21:19:43 +01:00
Max 493d836f96 omit --no-sdist 2023-01-28 21:12:21 +01:00
Max 984b899088 update maturin 2023-01-28 20:56:40 +01:00
Max 37471f18ec use PyO3's maturin action 2023-01-28 19:57:23 +01:00
Max 30d1502bdc update setup-python version in workflow 2023-01-28 19:16:33 +01:00
Max 4809e599dc v0.9.3 2023-01-28 19:06:09 +01:00
Max 8ab467b2c7 fixed object count in map attributes of converted maps 2023-01-28 18:59:28 +01:00
Max 92cf2e9bfc updated rosu-pp 2023-01-28 18:54:24 +01:00
Max 488ad77840 updated pyo3 2023-01-28 18:53:54 +01:00
Max 2d18ef664f fixed typos 2022-10-29 23:54:06 +02:00
32 changed files with 4389 additions and 2045 deletions
+242
View File
@@ -0,0 +1,242 @@
# Largely references https://github.com/pydantic/pydantic-core/blob/193f5d7a8627c988b1c7fab9a3712c779afbbe94/.github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
- next
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
pull_request:
workflow_dispatch:
jobs:
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Run clippy
run: cargo clippy --all-targets
build-sdist:
name: Build sdist
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
rust-toolchain: stable
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: pypi_files_sdist
path: dist
build:
name: Build on ${{ matrix.os }} (${{ matrix.target }} - ${{ matrix.interpreter || 'all' }}${{ matrix.os == 'linux' && format(' - {0}', matrix.manylinux == 'auto' && 'manylinux' || matrix.manylinux) || '' }})
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next'
strategy:
fail-fast: false
matrix:
os: [linux, macos, windows]
target: [x86_64, aarch64]
manylinux: [auto]
include:
# manylinux
- os: linux
manylinux: auto
target: aarch64
- os: linux
manylinux: auto
target: armv7
interpreter: 3.8 3.9 3.10 3.11 3.12 3.13
- os: linux
manylinux: auto
target: x86_64
# musllinux
- os: linux
manylinux: musllinux_1_1
target: x86_64
- os: linux
manylinux: musllinux_1_1
target: aarch64
# macos
- os: macos
target: x86_64
- os: macos
target: aarch64
# windows
- os: windows
target: x86_64
- os: windows
target: aarch64
interpreter: 3.11 3.12 3.13
runs-on: ${{ (matrix.os == 'linux' && 'ubuntu') || matrix.os }}-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: '3.11'
architecture: x64
- run: pip install -U twine 'ruff==0.1.3' typing_extensions
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.8 3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10' }}
rust-toolchain: 1.77.0
docker-options: -e CI
sccache: true
- run: twine check --strict dist/*
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: pypi_files_${{ matrix.os }}_${{ matrix.target }}_${{ matrix.interpreter || 'all' }}_${{ matrix.manylinux }}
path: dist
test-builds-arch:
name: Test build on ${{ matrix.target }}-${{ matrix.distro }}
needs: [build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [aarch64, armv7]
distro: ['ubuntu22.04']
include:
- target: aarch64
distro: alpine_latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Get artifacts
uses: actions/download-artifact@v4
with:
pattern: pypi_files_linux_*
merge-multiple: true
path: dist
- name: Install & test
uses: uraimo/run-on-arch-action@v2.7.1
with:
arch: ${{ matrix.target }}
distro: ${{ matrix.distro }}
githubToken: ${{ github.token }}
install: |
set -x
if command -v apt-get &> /dev/null; then
echo "installing python & pip with apt-get..."
apt-get update
apt-get install -y --no-install-recommends python3 python3-pip python3-venv git
else
echo "installing python & pip with apk..."
apk update
apk add python3 py3-pip git
fi
run: |
set -x
# typing-extensions isn't automatically installed because of `--no-index --no-deps`
python3 -m venv venv
source venv/bin/activate
python3 -m pip install -U pip typing-extensions
python3 -m pip install rosu_pp_py --no-index --no-deps --find-links dist --force-reinstall
python3 -c "import rosu_pp_py"
test-builds-os:
name: Test build on ${{ matrix.os }}
needs: [build]
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
runs-on: ${{ matrix.os }}-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Get artifacts
uses: actions/download-artifact@v4
with:
pattern: pypi_files_*
merge-multiple: true
path: dist
- run: |
pip install typing-extensions
pip install rosu_pp_py --no-index --no-deps --find-links dist --force-reinstall
python -c "import rosu_pp_py"
release:
name: Release
needs: [clippy, test-builds-arch, test-builds-os, build-sdist]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- run: pip install -U twine
- name: Get artifacts
uses: actions/download-artifact@v4
with:
pattern: pypi_files_*
merge-multiple: true
path: dist
- run: twine check --strict dist/*
- name: Upload to PyPI
run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
-150
View File
@@ -1,150 +0,0 @@
name: Publish
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
jobs:
macos:
runs-on: macos-13
strategy:
matrix:
python_version: ['3.7', '3.8', '3.9', '3.10', '3.11']
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Python setup
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python_version }}
architecture: x64
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Build wheels - x86_64
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist
- name: Install built wheel - x86_64
run: |
pip install akatsuki_pp_py --no-index --find-links dist --force-reinstall
python -c "import akatsuki_pp_py"
- name: Build wheels - universal2
uses: PyO3/maturin-action@v1
with:
args: --release --universal2 --out dist
- name: Install built wheel - universal2
run: |
pip install akatsuki_pp_py --no-index --find-links dist --force-reinstall
python -c "import akatsuki_pp_py"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
python_version: ['3.7', '3.8', '3.9', '3.10', '3.11']
target: [x64, x86]
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Python setup
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python_version }}
architecture: ${{ matrix.target }}
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
- name: Install built wheel
run: |
pip install akatsuki_pp_py --no-index --find-links dist --force-reinstall
python -c "import akatsuki_pp_py"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
linux:
runs-on: ubuntu-20.04
strategy:
matrix:
python_version: ['3.7', '3.8', '3.9', '3.10', '3.11']
target: [x86_64, i686, aarch64]
steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Python setup
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python_version }}
architecture: x64
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i ${{ matrix.python_version }}
- name: Install built wheel
if: matrix.target == 'x86_64'
run: |
pip install akatsuki_pp_py --no-index --find-links dist --force-reinstall
python -c "import akatsuki_pp_py"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
strategy:
matrix:
python_version: ['3.7', '3.8', '3.9', '3.10', '3.11']
needs: [ macos, windows, linux ]
steps:
- name: Get artifacts
uses: actions/download-artifact@v3
with:
name: wheels
- name: Python setup
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python_version }}
- name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
pip install --upgrade twine
twine upload --skip-existing *
+2
View File
@@ -1,4 +1,6 @@
/target
expanded.rs
output.rs
# Byte-compiled / optimized / DLL files
__pycache__/
+39 -1
View File
@@ -1,4 +1,40 @@
# v0.9.1 (2022-10-29)
# v1.1.0 (2024-07-12)
- Updated to [rosu-pp v1.1.0](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v110-2024-07-10)
- Mods can now be specified through more types than just `int` ([#9]). Instead, it has to coincide with the following alias defintions:
```py
GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]
GameMod = dict[str, Union[str, GameModSettings]]
GameModSettings = dict[str, Union[bool, float, str]]
```
That means, mods can be given either through their [(legacy) bitflags](https://github.com/ppy/osu-api/wiki#reference), a string for acronyms, a "GameMod" `dict`, or a sequence whose items are either a "GameMod" `dict`, a single acronym string, or bitflags for a single mod.
A "GameMod" `dict` **must** have the item `'acronym': str` and an optional item `'settings': GameModSettings`.
## v1.0.1 (2024-05-05)
- `PerformanceAttributes`' field `pp_accuracy` was accidentally actually named `pp_acc`; now it's definitely `pp_accuracy` ([#7])
## v1.0.0 (2024-04-03)
- Updated to [rosu-pp v1.0.0](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v100-2024-04-02)
- Breaking changes ahead! There are now multiple different calculators:
- `Difficulty` to calculate `DifficultyAttributes`, `Strains`, or create gradual calculators
- `Performance` to calculate `PerformanceAttributes`
- `BeatmapAttributesBuilder` to calculate `BeatmapAttributes`
- `GradualDifficulty` to calculate `DifficultyAttributes` for each hitobject
- `GradualPerformance` to calculate `PerformanceAttributes` for each hitresult
## v0.9.4 (2023-02-09)
- Updated to [rosu-pp v0.9.4](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v094-2023-02-09).
## v0.9.3 (2023-01-28)
- Updated to [rosu-pp v0.9.3](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v093-2023-01-28). Only includes some bug fixes.
- Fixed the hitobjects counts of map attributes on converted maps.
## v0.9.1 (2022-10-29)
- Updated to [rosu-pp v0.9.1](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v091-2022-10-26) including the big changes in [v0.9.0](https://github.com/MaxOhn/rosu-pp/blob/main/CHANGELOG.md#v090-2022-10-24)
- The binding interface is rewritten completely, see the readme.
@@ -42,3 +78,5 @@ osu!standard, osu!taiko, and osu!mania.
[@tsunyoku]: https://github.com/tsunyoku
[#3]: https://github.com/MaxOhn/rosu-pp-py/pull/3
[#7]: https://github.com/MaxOhn/rosu-pp-py/pull/7
[#9]: https://github.com/MaxOhn/rosu-pp-py/pull/9
-6
View File
@@ -1,6 +0,0 @@
* @tsunyoku @cmyui
# Infrastructure
/.github/* @cmyui
/tf/* @cmyui
/chart/* @cmyui
Generated
+106 -166
View File
@@ -2,31 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "akatsuki-pp"
version = "1.0.1"
source = "git+https://github.com/osuAkatsuki/akatsuki-pp-rs?rev=b96019f50aa7d09bcbde60016481a69df04315e4#b96019f50aa7d09bcbde60016481a69df04315e4"
[[package]]
name = "akatsuki-pp-py"
version = "1.0.5"
dependencies = [
"akatsuki-pp",
"pyo3",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -34,85 +15,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "indoc"
version = "1.0.8"
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "libc"
version = "0.2.139"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
]
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "memoffset"
version = "0.6.5"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.16.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "parking_lot"
version = "0.12.1"
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
]
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "proc-macro2"
version = "1.0.49"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.17.3"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543"
checksum = "1962a33ed2a201c637fc14a4e0fd4e06e6edfdeee6a5fede0dab55507ad74cf7"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
@@ -121,9 +82,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.17.3"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8"
checksum = "ab7164b2202753bd33afc7f90a10355a719aa973d1f94502c50d06f3488bc420"
dependencies = [
"once_cell",
"target-lexicon",
@@ -131,9 +92,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.17.3"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc"
checksum = "c6424906ca49013c0829c5c1ed405e20e2da2dc78b82d198564880a704e6a7b7"
dependencies = [
"libc",
"pyo3-build-config",
@@ -141,9 +102,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.17.3"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28"
checksum = "82b2f19e153122d64afd8ce7aaa72f06a00f52e34e1d1e74b6d71baea396460a"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -153,50 +114,86 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.17.3"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f"
checksum = "dd698c04cac17cf0fe63d47790ab311b8b25542f5cb976b65c374035c50f1eef"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rosu-map"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c55926c8f0fed1db12fbe96f7a6083a2c4186443dd32532ab34e6902467a4f3"
[[package]]
name = "rosu-mods"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d69daf02885f7477085403a6eada6215f44333c7b54355ea1c4e276a02263bde"
dependencies = [
"serde",
]
[[package]]
name = "rosu-pp"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "002a6b12cedcb185f4051f0b3d0466e0b61ff414a9ca8375f09be581c0e70f06"
dependencies = [
"rosu-map",
"rosu-mods",
]
[[package]]
name = "rosu-pp-py"
version = "1.1.0"
dependencies = [
"pyo3",
"rosu-mods",
"rosu-pp",
"serde",
]
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "syn"
version = "1.0.107"
version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [
"proc-macro2",
"quote",
@@ -205,75 +202,18 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.5"
version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d"
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "unicode-ident"
version = "1.0.6"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "unindent"
version = "0.1.11"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
+15 -10
View File
@@ -1,20 +1,25 @@
[package]
name = "akatsuki-pp-py"
version = "1.0.5"
description = "osu! difficulty and pp calculation for all modes"
authors = ["Max Ohn <ohn.m@hotmail.de>", "tsunyoku <tsunyoku@gmail.com>"]
license = "MIT"
name = "rosu-pp-py"
version = "1.1.0"
edition = "2021"
authors = ["Max Ohn <ohn.m@hotmail.de>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/MaxOhn/rosu-pp-py"
description = "Difficulty and performance calculation for osu!"
[lib]
name = "akatsuki_pp_py"
name = "rosu_pp_py"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.17", features = ["extension-module", "macros"] }
akatsuki-pp = { git = "https://github.com/osuAkatsuki/akatsuki-pp-rs", rev = "b96019f50aa7d09bcbde60016481a69df04315e4" }
pyo3 = { version = "0.22", features = ["extension-module", "macros"] }
rosu-mods = { version = "0.1.0", default-features = false, features = ["serde"] }
rosu-pp = { version = "1.0.0", features = ["sync"] }
serde = "1.0.203"
[profile.release]
lto = "fat"
lto = true
panic = "abort"
codegen-units = 1
strip = "debuginfo"
strip = true
+155 -71
View File
@@ -1,100 +1,184 @@
# akatsuki-pp-py
# rosu-pp-py
Difficulty and performance calculation for all [osu!](https://osu.ppy.sh/) modes.
Library to calculate difficulty and performance attributes for all [osu!] modes.
This is a python binding to the Rust library [rosu-pp](https://github.com/MaxOhn/rosu-pp) which was bootstrapped through [PyO3](https://github.com/PyO3/PyO3).
Since all the heavy lifting is done by Rust, rosu-pp-py comes with a very fast performance.
Check out rosu-pp's [README](https://github.com/MaxOhn/rosu-pp/blob/main/README.md) for more info.
This is a python binding to the [Rust] library [rosu-pp] which was bootstrapped through [PyO3].
As such, its performance is much faster than a native python library.
## Exposed types
## Usage
The library exposes the following classes:
- `Calculator`: Contains various parameters to calculate strains or map, difficulty, or performance attributes
- `Beatmap`: Contains a parsed beatmap
- [`BeatmapAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/81e1d6f28064b832661a4940a3896a2089f76b6b/rosu_pp_py.pyi#L199-L231): Contains various attributes about the map itself
- [`DifficultyAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/81e1d6f28064b832661a4940a3896a2089f76b6b/rosu_pp_py.pyi#L234-L284): Contains various attributes about the difficulty based on the mode
- [`PerformanceAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/81e1d6f28064b832661a4940a3896a2089f76b6b/rosu_pp_py.pyi#L287-L313): Contains various attributes about the performance and difficulty based on the mode
- [`Strains`](https://github.com/MaxOhn/rosu-pp-py/blob/81e1d6f28064b832661a4940a3896a2089f76b6b/rosu_pp_py.pyi#L316-L346): Contains strain values for each skill based on the mode
Additionally, the following error types are exposed:
- `ParseError`: Failed to parse a beatmap
- `KwargsError`: Invalid kwargs were provided
## How to use akatsuki-pp-py
1) The first step is to create a new `Beatmap` instance by providing appropriate kwargs.
Either of the kwargs `path`, `content`, or `bytes` **must** be given. The kwargs `ar`, `cs`, `hp`, and `od` are optional.
With the setters `set_ar`, `set_cs`, `set_hp`, and `set_od` you can specify custom attributes.
```py
map = Beatmap(path = "/path/to/file.osu", ar = 9.87)
map.set_od(1.23)
with open("/path/to/file.osu", "rb") as file:
map = Beatmap(bytes = file.read())
with open("/path/to/file.osu") as file:
map = Beatmap(content = file.read())
```
2) Next, you need to create an instance of `Calculator` by providing the appropriate kwargs again.
Any of the following kwargs are allowed: `mode`, `mods`, `acc`, `n_geki`, `n_katu`, `n300`, `n100`, `n50`, `n_misses`, `combo`, `passed_objects`, `clock_rate`, and `difficulty`.
Each of these also have a setter method e.g. `set_n_misses`.
```py
calc = Calculator(mode = 2, acc = 98.76)
calc.set_mods(8 + 64) # HDDT
```
3) The last step is to call any of the methods `map_attributes`, `difficulty`, `performance`, or `strains` on the calculator and provide them a `Beatmap`.
The library exposes multiple classes:
- [`Beatmap`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L23-L101): Parsed `.osu` file
- [`GameMode`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L5-L13)
- Calculators
- [`Difficulty`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L103-L251): Class to calculate difficulty attributes, strains, or create gradual calculators
- [`Performance`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L253-L439): Performance attributes calculator
- [`GradualDifficulty`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L441-L465): Calculator to calculate difficulty attributes after each hitobject
- [`GradualPerformance`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L467-L493): Calculator to calculator performance attributes after each hitresult
- [`BeatmapAttributesBuilder`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L495-L617): Beatmap attributes calculator
- Results
- [`DifficultyAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L666-L853)
- [`Strains`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L928-L998): Strain values of a difficulty calculation, suitable to plot difficulty over time
- [`PerformanceAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L855-L926)
- [`BeatmapAttributes`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L1000-L1030)
- [`HitResultPriority`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L15-L21): Passed to `Performance`, decides whether specified accuracy should be realized through good or bad hitresults
- [`ScoreState`](https://github.com/MaxOhn/rosu-pp-py/blob/f0f08fe9c9a94331840db67d5cf3b9b8cd15ea55/rosu_pp_py.pyi#L619-L664): Hitresults and max combo of a score, found in `PerformanceAttributes` and passed to gradual calculators
## Example
### Calculating performance
```py
from akatsuki_pp_py import Beatmap, Calculator
import rosu_pp_py as rosu
map = Beatmap(path = "./maps/100.osu")
calc = Calculator(mods = 8)
# either `path`, `bytes`, or `content` must be specified when parsing a map
map = rosu.Beatmap(path = "/path/to/file.osu")
# Calculate an SS on HD
max_perf = calc.performance(map)
# Optionally convert to a specific mode
map.convert(rosu.GameMode.Mania)
# The mods are still set to HD
calc.set_acc(99.11)
calc.set_n_misses(1)
calc.set_combo(200)
perf = rosu.Performance(
# various kwargs available
accuracy = 98.76,
misses = 2,
combo = 700,
hitresult_priority = rosu.HitResultPriority.WorstCase, # favors bad hitresults
)
# A good way to speed up the calculation is to provide
# the difficulty attributes of a previous calculation
# so that they don't need to be recalculated.
# **Note** that this should only be done if neither
# the map, mode, mods, nor passed objects amount changed.
calc.set_difficulty(max_perf.difficulty)
# Each kwarg can also be specified afterwards through setters
perf.set_accuracy(99.11) # override previously specified accuracy
perf.set_mods(8 + 64) # HDDT
perf.set_clock_rate(1.4)
curr_perf = calc.performance(map)
print(f'PP: {curr_perf.pp}/{max_perf.pp} | Stars: {max_perf.difficulty.stars}')
# Second argument of map attributes specifies whether mods still need to be accounted for
# `True`: mods already considered; `False`: value should still be adjusted
perf.set_ar(10.5, True)
perf.set_od(5, False)
map_attrs = calc.map_attributes(map)
print(f'BPM: {map_attrs.bpm}')
# Calculate for the map
attrs = perf.calculate(map)
strains = calc.strains(map)
print(f'Maximum aim strain: {max(strains.aim)}')
# Note that calculating via map will have to calculate difficulty attributes
# internally which is fairly expensive. To speed it up, you can also pass in
# previously calculated attributes, but be sure they were calculated for the
# same difficulty settings like mods, clock rate, custom map attributes, ...
perf.set_accuracy(100)
perf.set_misses(None)
perf.set_combo(None)
# Calculate a new set of attributes by re-using previous attributes instead of the map
max_attrs = perf.calculate(attrs)
print(f'PP: {attrs.pp}/{max_attrs.pp} | Stars: {max_attrs.difficulty.stars}')
```
### Gradual calculation
```py
import rosu_pp_py as rosu
# Parsing the map, this time through the `content` kwarg
with open("/path/to/file.osu") as file:
map = rosu.Beatmap(content = file.read())
# Specifying some difficulty parameters
diff = rosu.Difficulty(
mods = 16 + 1024, # HRFL
clock_rate = 1.1,
ar = 10.2,
ar_with_mods = True,
)
# Gradually calculating *difficulty* attributes
gradual_diff = diff.gradual_difficulty(map)
for i, attrs in enumerate(gradual_diff, 1):
print(f'Stars after {i} hitobjects: {attrs.stars}')
# Gradually calculating *performance* attributes
gradual_perf = diff.gradual_performance(map)
i = 1
while True:
state = rosu.ScoreState(
max_combo = i,
n300 = i,
n100 = 0,
# ...
)
attrs = gradual_perf.next(state)
if attrs is None:
# All hitobjects have been processed
break
print(f'PP: {attrs.pp}')
i += 1
```
## Mods
Wherever mods are specified, their type should coincide with the following alias definition:
```py
GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]
GameMod = dict[str, Union[str, GameModSettings]]
GameModSettings = dict[str, Union[bool, float, str]]
```
That means, mods can be given either through their [(legacy) bitflags](https://github.com/ppy/osu-api/wiki#reference),
a string for acronyms, a "GameMod" `dict`, or a sequence whose items are either
a "GameMod" `dict`, a single acronym string, or bitflags for a single mod.
A "GameMod" `dict` **must** have the item `'acronym': str` and an optional item `'settings': GameModSettings`.
Some examples for valid mods look as follows:
```py
mods = 8 + 64 # Hidden, DoubleTime
mods = "hRNcWIez" # HardRock, Nightcore, Wiggle, Easy
mods = { 'acronym': "FI" } # FadeIn
mods = [
1024,
'nf',
{
'acronym': "AC",
'settings': {
'minimum_accuracy': 95,
'restart': True
}
}
] # Flashlight, NoFail, AccuracyChallenge
import json
mods_json = '[{"acronym": "TC"}, {"acronym": "HT", "settings": {"speed_change": 0.6}}]'
mods = json.loads(mods_json) # Traceable, HalfTime
```
## Installing rosu-pp-py
Installing rosu-pp-py requires a [supported version of Python and Rust](https://github.com/PyO3/PyO3#usage).
Once [Python](https://www.python.org/downloads/) and [Rust](https://www.rust-lang.org/learn/get-started) and ready to go, you can install the project with pip:
Once [Python] and [Rust](https://www.rust-lang.org/learn/get-started) are ready to go, you can install the project with pip:
```sh
$ pip install akatsuki-pp-py
$ pip install rosu-pp-py
```
or
```
$ pip install git+https://github.com/osuAkatsuki/akatsuki-pp-py
$ pip install git+https://github.com/MaxOhn/rosu-pp-py
```
## Learn More
- [Rust documentation](https://www.rust-lang.org).
- [PyO3 documentation](https://pyo3.rs/).
- [Python documentation](https://docs.python.org/3/).
- [rosu-pp]
- [Rust]
- [PyO3]
[osu!]: https://osu.ppy.sh/home
[Rust]: https://www.rust-lang.org
[rosu-pp]: https://github.com/MaxOhn/rosu-pp
[PyO3]: https://github.com/PyO3/pyo3
[Python]: https://www.python.org/downloads/
-512
View File
@@ -1,512 +0,0 @@
from typing import Optional
from typing import List
class Beatmap:
"""
A class containing a parsed beatmap.
The kwargs must include any of the following:
`'path': str`
The path to a .osu file
`'content': str | bytearray`
The content of a .osu file as string or bytes
`'bytes': bytearray`
The content of a .osu file as bytes
The kwargs may include any of the following:
`'ar': float`
Specify a custom approach rate
`'cs': float`
Specify a custom circle size
`'hp': float`
Specify a custom drain rate
`'od': float`
Specify a custom overall difficulty
## Raises
Throws an exception if the map could not be parsed or an invalid kwarg was given
"""
def __init__(self, **kwargs) -> None:
...
def set_ar(self, ar: float) -> None:
"""
Specify a custom approach rate
"""
...
def set_cs(self, cs: float) -> None:
"""
Specify a custom circle size
"""
...
def set_hp(self, hp: float) -> None:
"""
Specify a custom drain rate
"""
...
def set_od(self, od: float) -> None:
"""
Specify a custom overall difficulty
"""
...
class Calculator:
"""
A class containing various attributes to calculate strains or map, difficulty, or performance attributes.
The kwargs may include any of the following:
`'mode': int`
Must be 0 for osu!standard, 1 for taiko, 2 for catch, or 3 for mania
`'mods': int`
Bitflags for mods, see https://github.com/ppy/osu-api/wiki#mods
`'acc': float`
The accuracy between 0.0 and 100.0
`'n_geki': int`
The amount of gekis i.e. n320 in mania
`'n_katu': int`
The amount of katu i.e. tiny droplet misses in catch and n200 in mania
`'n300': int`
The amount of n300
`'n100': int`
The amount of n100
`'n50': int`
The amount of n50
`'n_misses': int`
The amount of misses
`'combo': int`
The max combo of the score
`'passed_object': int`
The amount of passed objects, handy for partial plays like fails
`'clock_rate': float`
Specify a custom clock rate
`'difficulty': DifficultyAttributes`
If you perform multiple calculations and neither map, mode, mods, nor passed objects amount change,
pass the difficulty attributes from a previous calculation so that they don't have to be recalculated
## Raises
Throws an exception if an invalid kwarg was given
"""
def __init__(self, **kwargs) -> None: ...
def set_mode(self, mode: int) -> None:
"""
Must be 0 for osu!standard, 1 for taiko, 2 for catch, or 3 for mania
"""
...
def set_mods(self, mods: int) -> None:
"""
Bitflags for mods, see https://github.com/ppy/osu-api/wiki#mods
"""
...
def set_acc(self, acc: float) -> None:
"""
The accuracy between 0.0 and 100.0
"""
...
def set_n_geki(self, n_geki: int) -> None:
"""
The amount of gekis i.e. n320 in mania
"""
...
def set_n_katu(self, n_katu: int) -> None:
"""
The amount of katu i.e. tiny droplet misses in catch and n200 in mania
"""
...
def set_n300(self, n300: int) -> None:
"""
The amount of n300
"""
...
def set_n100(self, n100: int) -> None:
"""
The amount of n100
"""
...
def set_n50(self, n50: int) -> None:
"""
The amount of n50
"""
...
def set_n_misses(self, n_misses: int) -> None:
"""
The amount of misses
"""
...
def set_combo(self, combo: int) -> None:
"""
The max combo of the score
"""
...
def set_passed_objects(self, passed_objects: int) -> None:
"""
The amount of passed objects, handy for partial plays like fails
"""
...
def set_clock_rate(self, clock_rate: float) -> None:
"""
Specify a custom clock rate
"""
...
def set_difficulty(self, difficulty: DifficultyAttributes) -> None:
"""
If you perform multiple calculations and neither map, mode, mods, nor passed objects change,
pass the difficulty attributes from a previous calculation so that they don't have to be recalculated
"""
...
def map_attributes(self, map: Beatmap) -> BeatmapAttributes:
"""
Based on the specified mods and clock rate, calculate the beatmap attributes for the given map
"""
...
def difficulty(self, map: Beatmap) -> DifficultyAttributes:
"""
Based on all specified parameters, calculate the difficulty attributes for the given map
"""
...
def performance(self, map: Beatmap) -> PerformanceAttributes:
"""
Based on all specified parameters, calculate the performance attributes for the given map
"""
...
def strains(self, map: Beatmap) -> Strains:
"""
Based on all specified parameters, calculate the strains for the given map
"""
...
class BeatmapAttributes:
"""
Various attributes of a beatmap
## Attributes
`'ar': float`
Approach rate
`'cs': float`
Circle size
`'hp': float`
Drain rate
`'od': float`
Overall difficulty
`'ar_hit_window': float`
Time in ms the the circle is visible ("time preempt")
`'od_hit_window': float`
Time in ms to get an n300 hitresult ("great hit window")
`'clock_rate': float`
Clock rate
`'bpm': float`
Beats per minute
`'mode': int`
Gamemode integer
`'version': int`
Version of the .osu file
`'n_circles': int`
Amount of circles
`'n_sliders': int`
Amount of sliders
`'n_spinners': int`
Amount of spinners
"""
@property
def ar(self) -> float: ...
@property
def cs(self) -> float: ...
@property
def hp(self) -> float: ...
@property
def od(self) -> float: ...
@property
def ar_hit_window(self) -> float: ...
@property
def od_hit_window(self) -> float: ...
@property
def clock_rate(self) -> float: ...
@property
def bpm(self) -> float: ...
@property
def mode(self) -> int: ...
@property
def version(self) -> int: ...
@property
def n_circles(self) -> int: ...
@property
def n_sliders(self) -> int: ...
@property
def n_spinners(self) -> int: ...
class DifficultyAttributes:
"""
All difficulty attributes depending on the mode.
## Attributes
The parantheses indicate for which mode the optional values will be available.
`'mode': int`
Gamemode integer
`'stars': float`
Star rating
`'max_combo': int`
Max combo
`'aim': Optional[float]`
Aim based portion of the star rating (O)
`'speed': Optional[float]`
Speed based portion of the star rating (O)
`'flashlight': Optional[float]`
Flashlight based portion of the star rating (O)
`'slider_factor': Optional[float]`
Nerf factor for aim based on slider difficulty (O)
`'speed_note_count': Optional[float]`
Amount of notes that are considered as difficult regarding speed (O)
`'ar': Optional[float]`
Approach rate (O, T)
`'od': Optional[float]`
Overall difficulty (O)
`'n_circles': Optional[int]`
Amount of circles (O)
`'n_sliders': Optional[int]`
Amount of sliders (O)
`'n_spinners': Optional[int]`
Amount of spinners (O)
`'stamina': Optional[float]`
Stamina based portion of the star rating (T)
`'color': Optional[float]`
Color based portion of the star rating (T)
`'rhythm': Optional[float]`
Rhythm based portion of the star rating (T)
`'peak': Optional[float]`
Combination of stamina, color, and rhythm ratings (T)
`'hit_window': Optional[float]`
Great hit window (T, M)
`'n_fruits': Optional[int]`
Amount of fruits (C)
`'n_droplets': Optional[int]`
Amount of droplets (C)
`'n_tiny_droplets': Optional[int]`
Amount of tiny droplets (C)
"""
@property
def mode(self) -> int: ...
@property
def stars(self) -> float: ...
@property
def max_combo(self) -> int: ...
@property
def aim(self) -> Optional[float]: ...
@property
def speed(self) -> Optional[float]: ...
@property
def flashlight(self) -> Optional[float]: ...
@property
def slider_factor(self) -> Optional[float]: ...
@property
def speed_note_count(self) -> Optional[float]: ...
@property
def ar(self) -> Optional[float]: ...
@property
def od(self) -> Optional[float]: ...
@property
def n_circles(self) -> Optional[int]: ...
@property
def n_sliders(self) -> Optional[int]: ...
@property
def n_spinners(self) -> Optional[int]: ...
@property
def stamina(self) -> Optional[float]: ...
@property
def color(self) -> Optional[float]: ...
@property
def rhythm(self) -> Optional[float]: ...
@property
def peak(self) -> Optional[float]: ...
@property
def hit_window(self) -> Optional[float]: ...
@property
def n_fruits(self) -> Optional[int]: ...
@property
def n_droplets(self) -> Optional[int]: ...
@property
def n_tiny_droplets(self) -> Optional[int]: ...
class PerformanceAttributes:
"""
All performance attributes depending on the mode.
## Attributes
The parantheses indicate for which mode the optional values will be available.
`'mode': int`
Gamemode integer
`'pp': float`
Performance points
`'difficulty': DifficultyAttributes`
Difficulty attributes based on the mode
`'pp_acc': Optional[float]`
Accuracy based portion of the performance points (O, T)
`'pp_aim': Optional[float]`
Aim based portion of the performance points (O)
`'pp_speed': Optional[float]`
Speed based portion of the performance points (O)
`'pp_flashlight': Optional[float]`
Flashlight based portion of the performance points (O)
`'effective_miss_count': Optional[float]`
Approximated misses including actual misses and assumed slider breaks (O, T)
`'pp_difficulty': Optional[float]`
Difficulty based portion of the performance points (T, M)
"""
@property
def mode(self) -> int: ...
@property
def pp(self) -> float: ...
@property
def difficulty(self) -> DifficultyAttributes: ...
@property
def pp_acc(self) -> Optional[float]: ...
@property
def pp_aim(self) -> Optional[float]: ...
@property
def pp_speed(self) -> Optional[float]: ...
@property
def pp_flashlight(self) -> Optional[float]: ...
@property
def effective_miss_count(self) -> Optional[float]: ...
@property
def pp_difficulty(self) -> Optional[float]: ...
class Strains:
"""
All strain values depending on the mode
## Attributes
The parantheses indicate for which mode the optional values will be available.
`'mode': int`
Gamemode integer
`'section_len': float`
Time in ms between two strain points
`'aim': Optional[List[float]]`
Aim strain values (O)
`'aim_no_sliders': Optional[List[float]]`
Aim strain values with sliders (O)
`'speed': Optional[List[float]]`
Speed strain values (O)
`'flashlight': Optional[List[float]]`
Flashlight strain values (O)
`'color': Optional[List[float]]`
Color strain values (T)
`'stamina': Optional[List[float]]`
Stamina strain values (T)
`'rhythm': Optional[List[float]]`
Rhythm strain values (T)
`'movement': Optional[List[float]]`
Movement strain values (C)
`'strains': Optional[List[float]]`
Strain values (M)
"""
@property
def mode(self) -> int: ...
@property
def section_len(self) -> float: ...
@property
def aim(self) -> Optional[List[float]]: ...
@property
def aim_no_sliders(self) -> Optional[List[float]]: ...
@property
def speed(self) -> Optional[List[float]]: ...
@property
def flashlight(self) -> Optional[List[float]]: ...
@property
def color(self) -> Optional[List[float]]: ...
@property
def stamina(self) -> Optional[List[float]]: ...
@property
def rhythm(self) -> Optional[List[float]]: ...
@property
def movement(self) -> Optional[List[float]]: ...
@property
def strains(self) -> Optional[List[float]]: ...
+8 -11
View File
@@ -1,14 +1,13 @@
[build-system]
requires = ["maturin>=0.14,<0.15"]
requires = ["maturin>=1,<2"]
build-backend = "maturin"
[project]
name = "akatsuki-pp-py"
version = "1.0.5"
name = "rosu-pp-py"
version = "1.1.0"
requires-python = ">=3.7"
description = "osu! difficulty and pp calculation for all modes"
description = "Difficulty and performance calculation for osu!"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
@@ -16,12 +15,10 @@ classifiers = [
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
packages = [{ include = "akatsuki_pp_py" }]
authors = [
{ name = "Max Ohn", email = "ohn.m@hotmail.de" },
{ name = "tsunyoku", email = "tsunyoku@gmail.com" },
]
authors = [{ name = "Max Ohn", email = "ohn.m@hotmail.de" }]
readme = "README.md"
license = { file = "LICENSE" }
repository = "https://github.com/osuAkatsuki/akatsuki-pp-py"
keywords = ["osu", "pp", "stars", "performance", "difficulty"]
[tool.maturin]
features = ["pyo3/extension-module", "pyo3/generate-import-lib"]
+1064
View File
File diff suppressed because it is too large Load Diff
+261
View File
@@ -0,0 +1,261 @@
use pyo3::{
exceptions::PyTypeError,
pyclass, pymethods,
types::{PyAnyMethods, PyDict},
Bound, PyRef, PyResult,
};
use rosu_pp::model::beatmap::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows};
use crate::{beatmap::PyBeatmap, error::ArgsError, mode::PyGameMode, mods::PyGameMods};
#[pyclass(name = "BeatmapAttributesBuilder")]
#[derive(Default)]
pub struct PyBeatmapAttributesBuilder {
mode: Option<PyGameMode>,
is_convert: bool,
mods: PyGameMods,
clock_rate: Option<f64>,
ar: Option<f32>,
ar_with_mods: bool,
cs: Option<f32>,
cs_with_mods: bool,
hp: Option<f32>,
hp_with_mods: bool,
od: Option<f32>,
od_with_mods: bool,
}
#[pymethods]
impl PyBeatmapAttributesBuilder {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs {
match key.extract()? {
"map" => {
let map = value
.extract::<PyRef<'_, PyBeatmap>>()
.map_err(|_| PyTypeError::new_err("kwarg 'map': must be a Beatmap"))?;
this.set_map(map);
}
"mode" => {
this.mode =
Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'mode': must be a GameMode")
})?)
}
"is_convert" => {
this.is_convert = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'is_convert': must be a bool"))?
}
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be GameMods"))?
}
"clock_rate" => {
this.clock_rate =
Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'clock_rate': must be a float")
})?)
}
"ar" => {
this.ar = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar': must be a float"))?,
)
}
"ar_with_mods" => {
this.ar_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar_with_mods': must be a bool"))?
}
"cs" => {
this.cs = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs': must be a float"))?,
)
}
"cs_with_mods" => {
this.cs_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs_with_mods': must be a bool"))?
}
"hp" => {
this.hp = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp': must be a float"))?,
)
}
"hp_with_mods" => {
this.hp_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp_with_mods': must be a bool"))?
}
"od" => {
this.od = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od': must be a float"))?,
)
}
"od_with_mods" => {
this.od_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od_with_mods': must be a bool"))?
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'map', 'mode', \n\
'is_convert', 'mods', 'clock_rate', 'ar', 'ar_with_mods', \n\
'cs', 'cs_with_mods', 'hp', 'hp_with_mods', 'od', \n\
or 'od_with_mods'"
);
return Err(ArgsError::new_err(err));
}
}
}
Ok(this)
}
fn build(&self) -> PyBeatmapAttributes {
let mut builder = BeatmapAttributesBuilder::new();
builder = match self.mods {
PyGameMods::Lazer(ref mods) => builder.mods(mods.clone()),
PyGameMods::Intermode(ref mods) => builder.mods(mods),
PyGameMods::Legacy(mods) => builder.mods(mods),
};
if let Some(mode) = self.mode {
builder = builder.mode(mode.into(), self.is_convert);
}
if let Some(clock_rate) = self.clock_rate {
builder = builder.clock_rate(clock_rate);
}
if let Some(ar) = self.ar {
builder = builder.ar(ar, self.ar_with_mods);
}
if let Some(cs) = self.cs {
builder = builder.cs(cs, self.cs_with_mods);
}
if let Some(hp) = self.hp {
builder = builder.hp(hp, self.hp_with_mods);
}
if let Some(od) = self.od {
builder = builder.od(od, self.od_with_mods);
}
builder.build().into()
}
fn set_map(&mut self, map: PyRef<'_, PyBeatmap>) {
let map = &map.inner;
self.mode = Some(map.mode.into());
self.ar = Some(map.ar);
self.cs = Some(map.cs);
self.hp = Some(map.hp);
self.od = Some(map.od);
self.is_convert = map.is_convert;
}
#[pyo3(signature = (mode, is_convert))]
fn set_mode(&mut self, mode: Option<PyGameMode>, is_convert: bool) {
self.mode = mode;
self.is_convert = is_convert;
}
#[pyo3(signature = (mods=None))]
fn set_mods(&mut self, mods: Option<PyGameMods>) {
self.mods = mods.unwrap_or_default();
}
#[pyo3(signature = (clock_rate=None))]
fn set_clock_rate(&mut self, clock_rate: Option<f64>) {
self.clock_rate = clock_rate;
}
#[pyo3(signature = (ar, ar_with_mods))]
fn set_ar(&mut self, ar: Option<f32>, ar_with_mods: bool) {
self.ar = ar;
self.ar_with_mods = ar_with_mods;
}
#[pyo3(signature = (cs, cs_with_mods))]
fn set_cs(&mut self, cs: Option<f32>, cs_with_mods: bool) {
self.cs = cs;
self.cs_with_mods = cs_with_mods;
}
#[pyo3(signature = (hp, hp_with_mods))]
fn set_hp(&mut self, hp: Option<f32>, hp_with_mods: bool) {
self.hp = hp;
self.hp_with_mods = hp_with_mods;
}
#[pyo3(signature = (od, od_with_mods))]
fn set_od(&mut self, od: Option<f32>, od_with_mods: bool) {
self.od = od;
self.od_with_mods = od_with_mods;
}
}
define_class! {
#[pyclass(name = "BeatmapAttributes", frozen)]
#[derive(Clone)]
pub struct PyBeatmapAttributes {
pub ar: f64!,
pub od: f64!,
pub cs: f64!,
pub hp: f64!,
pub clock_rate: f64!,
pub ar_hitwindow: f64!,
pub od_hitwindow: f64!,
}
}
impl From<BeatmapAttributes> for PyBeatmapAttributes {
fn from(attrs: BeatmapAttributes) -> Self {
let BeatmapAttributes {
ar,
od,
cs,
hp,
clock_rate,
hit_windows:
HitWindows {
ar: ar_hitwindow,
od: od_hitwindow,
},
} = attrs;
Self {
ar,
od,
cs,
hp,
clock_rate,
ar_hitwindow,
od_hitwindow,
}
}
}
+287
View File
@@ -0,0 +1,287 @@
use pyo3::{pyclass, PyErr};
use rosu_pp::{
any::DifficultyAttributes, catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
};
use crate::{error::ParseError, mode::PyGameMode};
define_class! {
#[pyclass(name = "DifficultyAttributes", frozen)]
#[derive(Clone, Default)]
pub struct PyDifficultyAttributes {
pub mode: PyGameMode!,
pub stars: f64!,
pub is_convert: bool!,
pub aim: f64?,
pub speed: f64?,
pub flashlight: f64?,
pub slider_factor: f64?,
pub speed_note_count: f64?,
pub od: f64?,
pub hp: f64?,
pub n_circles: u32?,
pub n_sliders: u32?,
pub n_spinners: u32?,
pub stamina: f64?,
pub rhythm: f64?,
pub color: f64?,
pub peak: f64?,
pub n_fruits: u32?,
pub n_droplets: u32?,
pub n_tiny_droplets: u32?,
pub n_objects: u32?,
pub ar: f64?,
pub hit_window: f64?,
pub max_combo: u32!,
}
}
impl From<OsuDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: OsuDifficultyAttributes) -> Self {
let OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
} = attrs;
Self {
mode: PyGameMode::Osu,
stars,
is_convert: false,
aim: Some(aim),
speed: Some(speed),
flashlight: Some(flashlight),
slider_factor: Some(slider_factor),
speed_note_count: Some(speed_note_count),
ar: Some(ar),
od: Some(od),
hp: Some(hp),
n_circles: Some(n_circles),
n_sliders: Some(n_sliders),
n_spinners: Some(n_spinners),
max_combo,
..Self::default()
}
}
}
impl From<TaikoDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: TaikoDifficultyAttributes) -> Self {
let TaikoDifficultyAttributes {
stamina,
rhythm,
color,
peak,
hit_window,
stars,
max_combo,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Taiko,
stars,
is_convert,
stamina: Some(stamina),
rhythm: Some(rhythm),
color: Some(color),
peak: Some(peak),
hit_window: Some(hit_window),
max_combo,
..Self::default()
}
}
}
impl From<CatchDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: CatchDifficultyAttributes) -> Self {
let max_combo = attrs.max_combo();
let CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Catch,
stars,
is_convert,
ar: Some(ar),
n_fruits: Some(n_fruits),
n_droplets: Some(n_droplets),
n_tiny_droplets: Some(n_tiny_droplets),
max_combo,
..Self::default()
}
}
}
impl From<ManiaDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: ManiaDifficultyAttributes) -> Self {
let ManiaDifficultyAttributes {
stars,
hit_window,
n_objects,
max_combo,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Mania,
stars,
is_convert,
hit_window: Some(hit_window),
n_objects: Some(n_objects),
max_combo,
..Self::default()
}
}
}
impl From<DifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: DifficultyAttributes) -> Self {
match attrs {
DifficultyAttributes::Osu(attrs) => attrs.into(),
DifficultyAttributes::Taiko(attrs) => attrs.into(),
DifficultyAttributes::Catch(attrs) => attrs.into(),
DifficultyAttributes::Mania(attrs) => attrs.into(),
}
}
}
impl TryFrom<PyDifficultyAttributes> for DifficultyAttributes {
type Error = PyErr;
fn try_from(attrs: PyDifficultyAttributes) -> Result<Self, Self::Error> {
let PyDifficultyAttributes {
mode,
stars,
is_convert,
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stamina,
rhythm,
color,
peak,
n_fruits,
n_droplets,
n_tiny_droplets,
n_objects,
ar,
hit_window,
max_combo,
} = attrs;
match mode {
PyGameMode::Osu => {
if let (
Some(aim),
Some(speed),
Some(flashlight),
Some(slider_factor),
Some(speed_note_count),
Some(ar),
Some(od),
Some(hp),
Some(n_circles),
Some(n_sliders),
Some(n_spinners),
) = (
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
) {
return Ok(Self::Osu(OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
}));
}
}
PyGameMode::Taiko => {
if let (Some(stamina), Some(rhythm), Some(color), Some(peak), Some(hit_window)) =
(stamina, rhythm, color, peak, hit_window)
{
return Ok(Self::Taiko(TaikoDifficultyAttributes {
stamina,
rhythm,
color,
peak,
hit_window,
stars,
max_combo,
is_convert,
}));
}
}
PyGameMode::Catch => {
if let (Some(ar), Some(n_fruits), Some(n_droplets), Some(n_tiny_droplets)) =
(ar, n_fruits, n_droplets, n_tiny_droplets)
{
return Ok(Self::Catch(CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
is_convert,
}));
}
}
PyGameMode::Mania => {
if let (Some(hit_window), Some(n_objects)) = (hit_window, n_objects) {
return Ok(Self::Mania(ManiaDifficultyAttributes {
stars,
hit_window,
n_objects,
max_combo,
is_convert,
}));
}
}
}
Err(ParseError::new_err("invalid difficulty attributes"))
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod beatmap;
pub mod difficulty;
pub mod performance;
+112
View File
@@ -0,0 +1,112 @@
use pyo3::pyclass;
use rosu_pp::{
any::PerformanceAttributes, catch::CatchPerformanceAttributes,
mania::ManiaPerformanceAttributes, osu::OsuPerformanceAttributes,
taiko::TaikoPerformanceAttributes,
};
use crate::score_state::PyScoreState;
use super::difficulty::PyDifficultyAttributes;
define_class! {
#[pyclass(name = "PerformanceAttributes", frozen)]
#[derive(Clone, Default)]
pub struct PyPerformanceAttributes {
pub difficulty: PyDifficultyAttributes!,
pub state: PyScoreState?,
pub pp: f64!,
pub pp_aim: f64?,
pub pp_flashlight: f64?,
pub pp_speed: f64?,
pub pp_accuracy: f64?,
pub effective_miss_count: f64?,
pub pp_difficulty: f64?,
}
}
impl From<OsuPerformanceAttributes> for PyPerformanceAttributes {
fn from(attrs: OsuPerformanceAttributes) -> Self {
let OsuPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
effective_miss_count,
} = attrs;
Self {
difficulty: difficulty.into(),
pp,
pp_accuracy: Some(pp_acc),
pp_aim: Some(pp_aim),
pp_flashlight: Some(pp_flashlight),
pp_speed: Some(pp_speed),
effective_miss_count: Some(effective_miss_count),
..Self::default()
}
}
}
impl From<TaikoPerformanceAttributes> for PyPerformanceAttributes {
fn from(attrs: TaikoPerformanceAttributes) -> Self {
let TaikoPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_difficulty,
effective_miss_count,
} = attrs;
Self {
difficulty: difficulty.into(),
pp,
pp_accuracy: Some(pp_acc),
pp_difficulty: Some(pp_difficulty),
effective_miss_count: Some(effective_miss_count),
..Self::default()
}
}
}
impl From<CatchPerformanceAttributes> for PyPerformanceAttributes {
fn from(attrs: CatchPerformanceAttributes) -> Self {
let CatchPerformanceAttributes { difficulty, pp } = attrs;
Self {
difficulty: difficulty.into(),
pp,
..Self::default()
}
}
}
impl From<ManiaPerformanceAttributes> for PyPerformanceAttributes {
fn from(attrs: ManiaPerformanceAttributes) -> Self {
let ManiaPerformanceAttributes {
difficulty,
pp,
pp_difficulty,
} = attrs;
Self {
difficulty: difficulty.into(),
pp,
pp_difficulty: Some(pp_difficulty),
..Self::default()
}
}
}
impl From<PerformanceAttributes> for PyPerformanceAttributes {
fn from(attrs: PerformanceAttributes) -> Self {
match attrs {
PerformanceAttributes::Osu(attrs) => attrs.into(),
PerformanceAttributes::Taiko(attrs) => attrs.into(),
PerformanceAttributes::Catch(attrs) => attrs.into(),
PerformanceAttributes::Mania(attrs) => attrs.into(),
}
}
}
+154 -85
View File
@@ -1,7 +1,23 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyResult};
use akatsuki_pp::Beatmap;
use std::{error::Error as StdError, fmt::Write};
use crate::error::{ErrorExt, KwargsError, ParseError};
use pyo3::{
exceptions::PyTypeError,
pyclass, pymethods,
types::{PyAnyMethods, PyDict},
Bound, PyResult,
};
use rosu_pp::{
model::{
hit_object::HitObjectKind,
mode::{ConvertStatus, GameMode},
},
Beatmap,
};
use crate::{
error::{ArgsError, ConvertError, ParseError},
mode::PyGameMode,
};
#[pyclass(name = "Beatmap")]
pub struct PyBeatmap {
@@ -11,34 +27,24 @@ pub struct PyBeatmap {
#[pymethods]
impl PyBeatmap {
#[new]
#[args(kwargs = "**")]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let kwargs = match kwargs {
Some(kwargs) => kwargs,
None => {
return Err(KwargsError::new_err(
"'Beatmap' constructor requires kwargs",
))
}
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
let Some(kwargs) = kwargs else {
return Err(ArgsError::new_err(
"kwarg 'path', 'bytes', or 'content' must be specified",
));
};
let mut map = None;
let mut ar = None;
let mut cs = None;
let mut hp = None;
let mut od = None;
let mut map_res = None;
for (key, value) in kwargs.iter() {
for (key, value) in kwargs {
match key.extract()? {
"path" => {
let path = value
.extract::<&str>()
.map_err(|_| PyTypeError::new_err("kwarg 'path': must be a string"))?;
.map_err(|_| PyTypeError::new_err("kwarg 'path': must be a str"))?;
let parsed = Beatmap::from_path(path)
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
map = Some(parsed);
map_res = Some(Beatmap::from_path(path));
}
"content" => {
let bytes = if let Ok(content) = value.extract::<&str>() {
@@ -47,100 +53,163 @@ impl PyBeatmap {
bytes
} else {
return Err(PyTypeError::new_err(
"kwarg 'content': must be a string or a bytearray",
"kwarg 'content': must be a str or a bytearray",
));
};
let parsed = Beatmap::from_bytes(bytes)
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
map = Some(parsed);
map_res = Some(Beatmap::from_bytes(bytes));
}
"bytes" => {
let bytes = value
.extract::<&[u8]>()
.map_err(|_| PyTypeError::new_err("kwarg 'bytes': must be a bytearray"))?;
let parsed = Beatmap::from_bytes(bytes)
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
map = Some(parsed);
}
"ar" => {
let value = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar': must be a real number"))?;
ar = Some(value);
}
"cs" => {
let value = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs': must be a real number"))?;
cs = Some(value);
}
"hp" => {
let value = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp': must be a real number"))?;
hp = Some(value);
}
"od" => {
let value = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od': must be a real number"))?;
od = Some(value);
map_res = Some(Beatmap::from_bytes(bytes));
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'path', \n\
'content', 'bytes', 'ar', 'cs', 'hp', or 'od'"
"unexpected kwarg '{kwarg}': expected 'path', \
'content', or 'bytes'"
);
return Err(KwargsError::new_err(err));
return Err(ArgsError::new_err(err));
}
}
}
let mut map = map.ok_or_else(|| {
KwargsError::new_err("kwargs must include 'path', 'content', or 'bytes'")
})?;
let map = match map_res {
Some(Ok(map)) => map,
Some(Err(err)) => {
let mut e = &err as &dyn StdError;
let mut content = format!("Failed to parse beatmap\n - caused by: {e}");
if let Some(ar) = ar {
map.ar = ar;
while let Some(src) = e.source() {
let _ = write!(content, "\n - caused by: {src}");
e = src;
}
if let Some(cs) = cs {
map.cs = cs;
return Err(ParseError::new_err(content));
}
if let Some(hp) = hp {
map.hp = hp;
}
if let Some(od) = od {
map.od = od;
None => {
return Err(ArgsError::new_err(
"kwarg 'path', 'bytes', or 'content' must be specified",
))
}
};
Ok(Self { inner: map })
}
fn set_ar(&mut self, ar: f32) {
self.inner.ar = ar;
fn convert(&mut self, mode: PyGameMode) -> PyResult<()> {
let mode = GameMode::from(mode);
if let ConvertStatus::Incompatible = self.inner.convert_in_place(mode) {
let err = format!("Cannot convert {:?} to {mode:?}", self.inner.mode);
return Err(ConvertError::new_err(err));
}
fn set_cs(&mut self, cs: f32) {
self.inner.cs = cs;
Ok(())
}
fn set_hp(&mut self, hp: f32) {
self.inner.hp = hp;
#[getter]
fn bpm(&self) -> f64 {
self.inner.bpm()
}
fn set_od(&mut self, od: f32) {
self.inner.od = od;
#[getter]
fn version(&self) -> i32 {
self.inner.version
}
#[getter]
fn is_convert(&self) -> bool {
self.inner.is_convert
}
#[getter]
fn stack_leniency(&self) -> f32 {
self.inner.stack_leniency
}
#[getter]
fn ar(&self) -> f32 {
self.inner.ar
}
#[getter]
fn cs(&self) -> f32 {
self.inner.cs
}
#[getter]
fn hp(&self) -> f32 {
self.inner.hp
}
#[getter]
fn od(&self) -> f32 {
self.inner.od
}
#[getter]
fn slider_multiplier(&self) -> f64 {
self.inner.slider_multiplier
}
#[getter]
fn slider_tick_rate(&self) -> f64 {
self.inner.slider_tick_rate
}
#[getter]
pub fn mode(&self) -> PyGameMode {
PyGameMode::from(self.inner.mode)
}
#[getter]
pub fn n_breaks(&self) -> usize {
self.inner.breaks.len()
}
#[getter]
pub fn n_objects(&self) -> usize {
self.inner.hit_objects.len()
}
#[getter]
pub fn n_circles(&self) -> usize {
self.inner
.hit_objects
.iter()
.filter(|h| h.is_circle())
.count()
}
#[getter]
pub fn n_sliders(&self) -> usize {
self.inner
.hit_objects
.iter()
.filter(|h| h.is_slider())
.count()
}
#[getter]
pub fn n_spinners(&self) -> usize {
self.inner
.hit_objects
.iter()
.filter(|h| h.is_spinner())
.count()
}
#[getter]
pub fn n_holds(&self) -> usize {
self.inner
.hit_objects
.iter()
.filter(|h| matches!(h.kind, HitObjectKind::Hold(_)))
.count()
}
}
-336
View File
@@ -1,336 +0,0 @@
use akatsuki_pp::{
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
osu_2019::OsuPP,
AnyPP, AnyStars, DifficultyAttributes, GameMode, Mods, PerformanceAttributes,
};
use pyo3::{
exceptions::{PyTypeError, PyValueError},
pyclass, pymethods,
types::PyDict,
PyResult,
};
use crate::{
beatmap::PyBeatmap, diff_attrs::PyDifficultyAttributes, error::KwargsError,
map_attrs::PyBeatmapAttributes, perf_attrs::PyPerformanceAttributes, strains::PyStrains,
};
#[pyclass(name = "Calculator")]
#[derive(Default)]
pub struct PyCalculator {
attributes: Option<DifficultyAttributes>,
mode: Option<GameMode>,
mods: Option<u32>,
acc: Option<f64>,
n_geki: Option<usize>,
n_katu: Option<usize>,
n300: Option<usize>,
n100: Option<usize>,
n50: Option<usize>,
n_misses: Option<usize>,
combo: Option<usize>,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
macro_rules! set_calc {
( $calc:ident, $this:ident: $( $field:ident ,)* ) => {
$(
if let Some(val) = $this.$field {
$calc = $calc.$field(val);
}
)*
};
}
#[pymethods]
impl PyCalculator {
#[new]
#[args(kwargs = "**")]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let kwargs = match kwargs {
Some(kwargs) => kwargs,
None => return Ok(Self::default()),
};
let mut this = Self::default();
for (key, value) in kwargs.iter() {
match key.extract()? {
"mode" => {
let int = value
.extract::<u8>()
.map_err(|_| PyTypeError::new_err("kwarg 'mode': must be an int"))?;
this.mode = match int {
0 => Some(GameMode::Osu),
1 => Some(GameMode::Taiko),
2 => Some(GameMode::Catch),
3 => Some(GameMode::Mania),
_ => return Err(PyValueError::new_err("invalid mode integer")),
}
}
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be an int"))?;
}
"n300" => {
this.n300 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n300': must be an int"))?;
}
"n100" => {
this.n100 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n100': must be an int"))?;
}
"n50" => {
this.n50 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n50': must be an int"))?;
}
"n_misses" => {
this.n_misses = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_misses': must be an int"))?;
}
"n_geki" => {
this.n_geki = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_geki': must be an int"))?;
}
"n_katu" => {
this.n_katu = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_katu': must be an int"))?;
}
"acc" | "accuracy" => {
this.acc = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'acc': must be a real number"))?;
}
"combo" => {
this.combo = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'combo': must be an int"))?;
}
"passed_objects" => {
this.passed_objects = value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'passed_objects': must be an int")
})?;
}
"clock_rate" => {
this.clock_rate = value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'clock_rate': must be a real number")
})?;
}
"difficulty" | "attributes" => {
let attrs = value.extract::<PyDifficultyAttributes>().map_err(|_| {
PyTypeError::new_err("kwarg 'difficulty': must be DifficultyAttributes")
})?;
this.attributes = Some(attrs.inner);
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'mode', 'mods', \n\
'n_geki', 'n_katu', 'n300', 'n100', 'n50', 'n_misses', \n\
'acc', 'combo', 'passed_objects', 'clock_rate', or 'difficulty'"
);
return Err(KwargsError::new_err(err));
}
}
}
Ok(this)
}
fn set_mods(&mut self, mods: u32) {
self.mods = Some(mods);
}
fn set_acc(&mut self, acc: f64) {
self.acc = Some(acc);
}
fn set_n_geki(&mut self, n_geki: usize) {
self.n_geki = Some(n_geki);
}
fn set_n_katu(&mut self, n_katu: usize) {
self.n_katu = Some(n_katu);
}
fn set_n300(&mut self, n300: usize) {
self.n300 = Some(n300);
}
fn set_n100(&mut self, n100: usize) {
self.n100 = Some(n100);
}
fn set_n50(&mut self, n50: usize) {
self.n50 = Some(n50);
}
fn set_n_misses(&mut self, n_misses: usize) {
self.n_misses = Some(n_misses);
}
fn set_combo(&mut self, combo: usize) {
self.combo = Some(combo);
}
fn set_passed_objects(&mut self, passed_objects: usize) {
self.passed_objects = Some(passed_objects);
}
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn set_difficulty(&mut self, difficulty: PyDifficultyAttributes) {
self.attributes = Some(difficulty.inner);
}
fn map_attributes(&self, map: &PyBeatmap) -> PyResult<PyBeatmapAttributes> {
let map = &map.inner;
let mut calc = map.attributes();
if let Some(mode) = self.mode {
calc.mode(mode);
if map.mode != mode && map.mode == GameMode::Osu {
calc.converted(true);
}
}
if let Some(mods) = self.mods {
calc.mods(mods);
}
if let Some(clock_rate) = self.clock_rate {
calc.clock_rate(clock_rate);
}
Ok(PyBeatmapAttributes::new(calc.build(), map))
}
fn difficulty(&self, map: &PyBeatmap) -> PyResult<PyDifficultyAttributes> {
let mut calc = AnyStars::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
passed_objects,
clock_rate,
};
Ok(calc.calculate().into())
}
fn performance_2019(&self, map: &PyBeatmap) -> PyResult<PyPerformanceAttributes> {
let mut calc = OsuPP::new(&map.inner);
set_calc! { calc, self:
mods,
combo,
n300,
n100,
n50,
passed_objects,
};
if let Some(n_misses) = self.n_misses {
calc = calc.misses(n_misses);
}
if let Some(acc) = self.acc {
calc = calc.accuracy(acc as f32);
}
let attrs = calc.calculate();
let new_attrs = OsuPerformanceAttributes {
difficulty: OsuDifficultyAttributes {
aim: attrs.difficulty.aim_strain,
speed: attrs.difficulty.speed_strain,
flashlight: 0.0,
slider_factor: 0.0,
speed_note_count: 0.0,
ar: attrs.difficulty.ar,
od: attrs.difficulty.od,
hp: attrs.difficulty.hp,
n_circles: attrs.difficulty.n_circles,
n_sliders: attrs.difficulty.n_sliders,
n_spinners: attrs.difficulty.n_spinners,
stars: attrs.difficulty.stars,
max_combo: attrs.difficulty.max_combo,
aim_difficult_strain_count: 0.0,
speed_difficult_strain_count: 0.0,
},
pp: attrs.pp,
pp_acc: attrs.pp_acc,
pp_aim: attrs.pp_aim,
pp_flashlight: attrs.pp_flashlight,
pp_speed: attrs.pp_speed,
effective_miss_count: attrs.effective_miss_count,
};
Ok(PerformanceAttributes::Osu(new_attrs).into())
}
fn performance(&self, map: &PyBeatmap) -> PyResult<PyPerformanceAttributes> {
// criteria:
// - is relax
// - is osu!standard
// or mode is not specified and map is osu!standard, as that will be the inferred mode
if (self.mods.is_some() && self.mods.unwrap().rx())
&& ((self.mode.is_none() && map.inner.mode == GameMode::Osu)
|| self.mode == Some(GameMode::Osu))
{
return self.performance_2019(map);
}
let mut calc = AnyPP::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
n_geki,
n_katu,
n300,
n100,
n50,
n_misses,
combo,
passed_objects,
clock_rate,
};
if let Some(ref attrs) = self.attributes {
calc = calc.attributes(attrs.to_owned());
}
if let Some(acc) = self.acc {
calc = calc.accuracy(acc);
}
Ok(calc.calculate().into())
}
fn strains(&self, map: &PyBeatmap) -> PyResult<PyStrains> {
let mut calc = AnyStars::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
passed_objects,
clock_rate,
};
Ok(calc.strains().into())
}
}
-239
View File
@@ -1,239 +0,0 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use akatsuki_pp::{
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes, DifficultyAttributes,
};
use pyo3::{pyclass, pymethods};
#[pyclass(name = "DifficultyAttributes")]
#[derive(Clone, Debug)]
pub struct PyDifficultyAttributes {
pub(crate) inner: DifficultyAttributes,
}
impl From<DifficultyAttributes> for PyDifficultyAttributes {
#[inline]
fn from(attrs: DifficultyAttributes) -> Self {
Self { inner: attrs }
}
}
impl Display for PyDifficultyAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let borrowed = BorrowedDifficultyAttributes::from(&self.inner);
Debug::fmt(&borrowed, f)
}
}
pub(crate) enum BorrowedDifficultyAttributes<'a> {
Osu(&'a OsuDifficultyAttributes),
Taiko(&'a TaikoDifficultyAttributes),
Catch(&'a CatchDifficultyAttributes),
Mania(&'a ManiaDifficultyAttributes),
}
macro_rules! impl_from {
( $( $mode:ident: $attrs:ident, )* ) => {
$(
impl<'a> From<&'a $attrs> for BorrowedDifficultyAttributes<'a> {
#[inline]
fn from(attrs: &'a $attrs) -> Self {
Self::$mode(attrs)
}
}
)*
impl<'a> From<&'a DifficultyAttributes> for BorrowedDifficultyAttributes<'a> {
#[inline]
fn from(attrs: &'a DifficultyAttributes) -> Self {
match attrs {
$( DifficultyAttributes::$mode(attrs) => Self::$mode(attrs), )*
}
}
}
};
}
impl_from! {
Osu: OsuDifficultyAttributes,
Taiko: TaikoDifficultyAttributes,
Catch: CatchDifficultyAttributes,
Mania: ManiaDifficultyAttributes,
}
impl Debug for BorrowedDifficultyAttributes<'_> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("DifficultyAttributes");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match self {
Self::Osu(attrs) => {
let OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
aim_difficult_strain_count,
speed_difficult_strain_count,
} = attrs;
debug.field("mode", &0_u8);
debug! {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
aim_difficult_strain_count,
speed_difficult_strain_count,
}
}
Self::Taiko(attrs) => {
let TaikoDifficultyAttributes {
stamina,
rhythm,
colour,
peak,
hit_window,
stars,
max_combo,
} = attrs;
debug.field("mode", &1_u8).field("color", colour);
debug!(stamina, rhythm, peak, hit_window, stars, max_combo);
}
Self::Catch(attrs) => {
let max_combo = attrs.max_combo();
let CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
} = attrs;
debug.field("mode", &2_u8);
debug!(stars, ar, n_fruits, n_droplets, n_tiny_droplets);
debug.field("max_combo", &max_combo);
}
Self::Mania(attrs) => {
let ManiaDifficultyAttributes {
stars,
hit_window,
max_combo,
} = attrs;
debug.field("mode", &3_u8);
debug!(stars, hit_window, max_combo);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$field:ident as $ty:ty: ( $( $mode:ident ),* ),
)*
) => {
#[pymethods]
impl PyDifficultyAttributes {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
DifficultyAttributes::Osu(_) => 0,
DifficultyAttributes::Taiko(_) => 1,
DifficultyAttributes::Catch(_) => 2,
DifficultyAttributes::Mania(_) => 3,
}
}
#[getter]
fn max_combo(&self) -> usize {
match &self.inner {
DifficultyAttributes::Osu(attrs) => attrs.max_combo,
DifficultyAttributes::Taiko(attrs) => attrs.max_combo,
DifficultyAttributes::Catch(attrs) => attrs.max_combo(),
DifficultyAttributes::Mania(attrs) => attrs.max_combo,
}
}
#[getter]
fn color(&self) -> Option<f64> {
if let DifficultyAttributes::Taiko(ref attrs) = self.inner {
Some(attrs.colour)
} else {
None
}
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
#[getter]
fn $field(&self) -> Option<$ty> {
match &self.inner {
$( DifficultyAttributes::$mode(attrs) => Some(attrs.$field), )*
#[allow(unreachable_patterns)]
_ => None,
}
}
)*
}
};
}
getters! {
stars as f64: (Osu, Taiko, Catch, Mania),
aim as f64: (Osu),
speed as f64: (Osu),
flashlight as f64: (Osu),
slider_factor as f64: (Osu),
speed_note_count as f64: (Osu),
od as f64: (Osu),
n_circles as usize: (Osu),
n_sliders as usize: (Osu),
n_spinners as usize: (Osu),
stamina as f64: (Taiko),
rhythm as f64: (Taiko),
peak as f64: (Taiko),
n_fruits as usize: (Catch),
n_droplets as usize: (Catch),
n_tiny_droplets as usize: (Catch),
ar as f64: (Osu, Catch),
hit_window as f64: (Taiko, Mania),
aim_difficult_strain_count as f64: (Osu),
speed_difficult_strain_count as f64: (Osu),
}
+268
View File
@@ -0,0 +1,268 @@
use pyo3::{
exceptions::PyTypeError,
pyclass, pymethods,
types::{PyAnyMethods, PyDict},
Bound, PyResult,
};
use rosu_pp::Difficulty;
use crate::{
attributes::difficulty::PyDifficultyAttributes,
beatmap::PyBeatmap,
error::ArgsError,
gradual::{difficulty::PyGradualDifficulty, performance::PyGradualPerformance},
mods::PyGameMods,
performance::PyPerformance,
strains::PyStrains,
};
#[pyclass(name = "Difficulty")]
#[derive(Default)]
pub struct PyDifficulty {
pub(crate) mods: PyGameMods,
pub(crate) clock_rate: Option<f64>,
pub(crate) ar: Option<f32>,
pub(crate) ar_with_mods: bool,
pub(crate) cs: Option<f32>,
pub(crate) cs_with_mods: bool,
pub(crate) hp: Option<f32>,
pub(crate) hp_with_mods: bool,
pub(crate) od: Option<f32>,
pub(crate) od_with_mods: bool,
pub(crate) passed_objects: Option<u32>,
pub(crate) hardrock_offsets: Option<bool>,
}
#[pymethods]
impl PyDifficulty {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs {
match key.extract()? {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be GameMods"))?
}
"clock_rate" => {
this.clock_rate =
Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'clock_rate': must be a float")
})?)
}
"ar" => {
this.ar = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar': must be a float"))?,
)
}
"ar_with_mods" => {
this.ar_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar_with_mods': must be a bool"))?
}
"cs" => {
this.cs = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs': must be a float"))?,
)
}
"cs_with_mods" => {
this.cs_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs_with_mods': must be a bool"))?
}
"hp" => {
this.hp = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp': must be a float"))?,
)
}
"hp_with_mods" => {
this.hp_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp_with_mods': must be a bool"))?
}
"od" => {
this.od = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od': must be a float"))?,
)
}
"od_with_mods" => {
this.od_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od_with_mods': must be a bool"))?
}
"passed_objects" => {
this.passed_objects = Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'passed_objects': must be an int")
})?)
}
"hardrock_offsets" => {
this.hardrock_offsets = Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'hardrock_offsets': must be a bool")
})?)
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'mods', \n\
'clock_rate', 'ar', 'ar_with_mods', 'cs', 'cs_with_mods', \n\
'hp', 'hp_with_mods', 'od', 'od_with_mods', \n\
'passed_objects', or 'hardrock_offsets'"
);
return Err(ArgsError::new_err(err));
}
}
}
Ok(this)
}
fn calculate(&self, map: &PyBeatmap) -> PyDifficultyAttributes {
self.as_difficulty().calculate(&map.inner).into()
}
fn strains(&self, map: &PyBeatmap) -> PyStrains {
self.as_difficulty().strains(&map.inner).into()
}
fn performance(&self) -> PyPerformance {
let Self {
mods,
clock_rate,
ar,
ar_with_mods,
cs,
cs_with_mods,
hp,
hp_with_mods,
od,
od_with_mods,
passed_objects,
hardrock_offsets,
} = self;
PyPerformance {
mods: mods.clone(),
clock_rate: *clock_rate,
ar: *ar,
ar_with_mods: *ar_with_mods,
cs: *cs,
cs_with_mods: *cs_with_mods,
hp: *hp,
hp_with_mods: *hp_with_mods,
od: *od,
od_with_mods: *od_with_mods,
passed_objects: *passed_objects,
hardrock_offsets: *hardrock_offsets,
..PyPerformance::default()
}
}
fn gradual_difficulty(&self, map: &PyBeatmap) -> PyGradualDifficulty {
PyGradualDifficulty::new(self, map)
}
fn gradual_performance(&self, map: &PyBeatmap) -> PyGradualPerformance {
PyGradualPerformance::new(self, map)
}
#[pyo3(signature = (mods=None))]
fn set_mods(&mut self, mods: Option<PyGameMods>) {
self.mods = mods.unwrap_or_default();
}
#[pyo3(signature = (clock_rate=None))]
fn set_clock_rate(&mut self, clock_rate: Option<f64>) {
self.clock_rate = clock_rate;
}
#[pyo3(signature = (ar, ar_with_mods))]
fn set_ar(&mut self, ar: Option<f32>, ar_with_mods: bool) {
self.ar = ar;
self.ar_with_mods = ar_with_mods;
}
#[pyo3(signature = (cs, cs_with_mods))]
fn set_cs(&mut self, cs: Option<f32>, cs_with_mods: bool) {
self.cs = cs;
self.cs_with_mods = cs_with_mods;
}
#[pyo3(signature = (hp, hp_with_mods))]
fn set_hp(&mut self, hp: Option<f32>, hp_with_mods: bool) {
self.hp = hp;
self.hp_with_mods = hp_with_mods;
}
#[pyo3(signature = (od, od_with_mods))]
fn set_od(&mut self, od: Option<f32>, od_with_mods: bool) {
self.od = od;
self.od_with_mods = od_with_mods;
}
#[pyo3(signature = (passed_objects=None))]
fn set_passed_objects(&mut self, passed_objects: Option<u32>) {
self.passed_objects = passed_objects;
}
#[pyo3(signature = (hardrock_offsets=None))]
fn set_hardrock_offsets(&mut self, hardrock_offsets: Option<bool>) {
self.hardrock_offsets = hardrock_offsets;
}
}
impl PyDifficulty {
pub fn as_difficulty(&self) -> Difficulty {
let mut difficulty = Difficulty::new();
difficulty = match self.mods {
PyGameMods::Lazer(ref mods) => difficulty.mods(mods.clone()),
PyGameMods::Intermode(ref mods) => difficulty.mods(mods),
PyGameMods::Legacy(mods) => difficulty.mods(mods),
};
if let Some(passed_objects) = self.passed_objects {
difficulty = difficulty.passed_objects(passed_objects);
}
if let Some(clock_rate) = self.clock_rate {
difficulty = difficulty.clock_rate(clock_rate);
}
if let Some(ar) = self.ar {
difficulty = difficulty.ar(ar, self.ar_with_mods);
}
if let Some(cs) = self.cs {
difficulty = difficulty.cs(cs, self.cs_with_mods);
}
if let Some(hp) = self.hp {
difficulty = difficulty.hp(hp, self.hp_with_mods);
}
if let Some(od) = self.od {
difficulty = difficulty.od(od, self.od_with_mods);
}
if let Some(hardrock_offsets) = self.hardrock_offsets {
difficulty = difficulty.hardrock_offsets(hardrock_offsets);
}
difficulty
}
}
+3 -22
View File
@@ -1,24 +1,5 @@
use std::{error::Error, fmt::Write};
use pyo3::{create_exception, exceptions::PyException};
create_exception!(akatsuki_pp_py, KwargsError, PyException);
create_exception!(akatsuki_pp_py, ParseError, PyException);
pub trait ErrorExt {
fn unwind(&self, cause: &str) -> String;
}
impl<T: Error> ErrorExt for T {
fn unwind(&self, cause: &str) -> String {
let mut e = self as &dyn Error;
let mut content = format!("{cause}\n - caused by: {e}");
while let Some(src) = e.source() {
let _ = write!(content, "\n - caused by: {src}");
e = src;
}
content
}
}
create_exception!(rosu_pp_py, ArgsError, PyException);
create_exception!(rosu_pp_py, ParseError, PyException);
create_exception!(rosu_pp_py, ConvertError, PyException);
+42
View File
@@ -0,0 +1,42 @@
use pyo3::{pyclass, pymethods, PyRef};
use rosu_pp::GradualDifficulty;
use crate::{
attributes::difficulty::PyDifficultyAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
};
#[pyclass(name = "GradualDifficulty")]
pub struct PyGradualDifficulty {
inner: GradualDifficulty,
}
#[pymethods]
impl PyGradualDifficulty {
#[new]
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
Self {
inner: GradualDifficulty::new(difficulty.as_difficulty(), &map.inner),
}
}
fn next(&mut self) -> Option<PyDifficultyAttributes> {
self.inner.next().map(From::from)
}
fn nth(&mut self, n: usize) -> Option<PyDifficultyAttributes> {
self.inner.nth(n).map(From::from)
}
#[getter]
fn n_remaining(&self) -> usize {
self.inner.len()
}
fn __iter__(this: PyRef<'_, Self>) -> PyRef<'_, Self> {
this
}
fn __next__(&mut self) -> Option<PyDifficultyAttributes> {
self.next()
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod difficulty;
pub mod performance;
+35
View File
@@ -0,0 +1,35 @@
use pyo3::{pyclass, pymethods};
use rosu_pp::GradualPerformance;
use crate::{
attributes::performance::PyPerformanceAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
score_state::PyScoreState,
};
#[pyclass(name = "GradualPerformance")]
pub struct PyGradualPerformance {
inner: GradualPerformance,
}
#[pymethods]
impl PyGradualPerformance {
#[new]
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
Self {
inner: GradualPerformance::new(difficulty.as_difficulty(), &map.inner),
}
}
fn next(&mut self, state: &PyScoreState) -> Option<PyPerformanceAttributes> {
self.inner.next(state.into()).map(From::from)
}
fn nth(&mut self, state: &PyScoreState, n: usize) -> Option<PyPerformanceAttributes> {
self.inner.nth(state.into(), n).map(From::from)
}
#[getter]
fn n_remaining(&self) -> usize {
self.inner.len()
}
}
+38 -17
View File
@@ -1,38 +1,59 @@
#![allow(unexpected_cfgs)]
#![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)]
use error::ConvertError;
use performance::PyHitResultPriority;
use pyo3::prelude::PyModuleMethods;
use pyo3::{pymodule, types::PyModule, Bound, PyResult, Python};
use self::{
attributes::{
beatmap::{PyBeatmapAttributes, PyBeatmapAttributesBuilder},
difficulty::PyDifficultyAttributes,
performance::PyPerformanceAttributes,
},
beatmap::PyBeatmap,
calculator::PyCalculator,
diff_attrs::PyDifficultyAttributes,
error::{KwargsError, ParseError},
map_attrs::PyBeatmapAttributes,
perf_attrs::PyPerformanceAttributes,
difficulty::PyDifficulty,
error::{ArgsError, ParseError},
gradual::{difficulty::PyGradualDifficulty, performance::PyGradualPerformance},
mode::PyGameMode,
performance::PyPerformance,
score_state::PyScoreState,
strains::PyStrains,
};
use pyo3::{pymodule, types::PyModule, PyResult, Python};
use strains::PyStrains;
#[macro_use]
mod macros;
mod attributes;
mod beatmap;
mod calculator;
mod diff_attrs;
mod difficulty;
mod error;
mod map_attrs;
mod perf_attrs;
mod gradual;
mod mode;
mod mods;
mod performance;
mod score_state;
mod strains;
#[pymodule]
fn akatsuki_pp_py(py: Python<'_>, m: &PyModule) -> PyResult<()> {
fn rosu_pp_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyBeatmap>()?;
m.add_class::<PyCalculator>()?;
m.add_class::<PyDifficulty>()?;
m.add_class::<PyPerformance>()?;
m.add_class::<PyGradualDifficulty>()?;
m.add_class::<PyGradualPerformance>()?;
m.add_class::<PyBeatmapAttributesBuilder>()?;
m.add_class::<PyGameMode>()?;
m.add_class::<PyScoreState>()?;
m.add_class::<PyHitResultPriority>()?;
m.add_class::<PyBeatmapAttributes>()?;
m.add_class::<PyDifficultyAttributes>()?;
m.add_class::<PyPerformanceAttributes>()?;
m.add_class::<PyStrains>()?;
m.add("ParseError", py.get_type::<ParseError>())?;
m.add("KwargsError", py.get_type::<KwargsError>())?;
m.add("ParseError", py.get_type_bound::<ParseError>())?;
m.add("ArgsError", py.get_type_bound::<ArgsError>())?;
m.add("ConvertError", py.get_type_bound::<ConvertError>())?;
Ok(())
}
+78
View File
@@ -0,0 +1,78 @@
pub struct BoolFormatter(pub bool);
impl std::fmt::Debug for BoolFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.0 { "True" } else { "False" })
}
}
macro_rules! define_class {
(
#[pyclass(name = $py_name:literal $(, $py_meta:meta)* )]
$( #[ $struct_meta:meta ] )*
$struct_vis:vis struct $name:ident {
$( $field_vis:vis $field:ident: $ty:ident $ty_type:tt , )*
}
) => {
#[pyclass(name = $py_name $(, $py_meta )* )]
$( #[ $struct_meta ] )*
$struct_vis struct $name {
$(
#[pyo3(get)]
$field_vis $field: define_class!(@EXPAND_TY $ty $ty_type),
)*
}
impl ::std::fmt::Debug for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
let mut debug = f.debug_struct($py_name);
macro_rules! debug_field {
( $inner_field:ident: $field_ty:ident ? ) => {
if let Some(ref $inner_field) = self.$inner_field {
debug.field(stringify!($inner_field), debug_field!(@VALUE $field_ty: $inner_field));
}
};
( $inner_field:ident: $field_ty:ident ! ) => {
let field = &self.$inner_field;
debug.field(stringify!($inner_field), debug_field!(@VALUE $field_ty: field));
};
( @VALUE bool: $value:tt ) => {
&crate::macros::BoolFormatter( *$value )
};
( @VALUE $field_ty:ident: $value:tt ) => {
&$value
};
}
$( debug_field!($field: $ty $ty_type); )*
debug.finish()
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Debug::fmt(self, f)
}
}
#[pyo3::pymethods]
impl $name {
fn __repr__(&self) -> String {
self.to_string()
}
}
};
( @EXPAND_TY $ty:ident ! ) => {
$ty
};
( @EXPAND_TY $ty:ident ? ) => {
Option<$ty>
};
}
-106
View File
@@ -1,106 +0,0 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use akatsuki_pp::{beatmap::BeatmapAttributes, Beatmap};
#[pyclass(name = "BeatmapAttributes")]
pub struct PyBeatmapAttributes {
#[pyo3(get)]
ar: f64,
#[pyo3(get)]
cs: f64,
#[pyo3(get)]
hp: f64,
#[pyo3(get)]
od: f64,
#[pyo3(get)]
ar_hit_window: f64,
#[pyo3(get)]
od_hit_window: f64,
#[pyo3(get)]
clock_rate: f64,
#[pyo3(get)]
bpm: f64,
#[pyo3(get)]
mode: u8,
#[pyo3(get)]
version: u8,
#[pyo3(get)]
n_circles: u32,
#[pyo3(get)]
n_sliders: u32,
#[pyo3(get)]
n_spinners: u32,
}
impl PyBeatmapAttributes {
pub fn new(attrs: BeatmapAttributes, map: &Beatmap) -> Self {
Self {
ar: attrs.ar,
cs: attrs.cs,
hp: attrs.hp,
od: attrs.od,
ar_hit_window: attrs.hit_windows.ar,
od_hit_window: attrs.hit_windows.od,
clock_rate: attrs.clock_rate,
bpm: map.bpm() * attrs.clock_rate,
mode: map.mode as u8,
version: map.version,
n_circles: map.n_circles,
n_sliders: map.n_sliders,
n_spinners: map.n_spinners,
}
}
}
impl Display for PyBeatmapAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let Self {
ar,
cs,
hp,
od,
ar_hit_window,
od_hit_window,
clock_rate,
bpm,
mode,
version,
n_circles,
n_sliders,
n_spinners,
} = self;
macro_rules! debug {
( $( $field:ident ,)* ) => {
f.debug_struct("BeatmapAttributes")
$( .field(stringify!($field), $field) )*
.finish()
};
}
debug! {
ar,
cs,
hp,
od,
ar_hit_window,
od_hit_window,
clock_rate,
bpm,
mode,
version,
n_circles,
n_sliders,
n_spinners,
}
}
}
#[pymethods]
impl PyBeatmapAttributes {
fn __repr__(&self) -> String {
self.to_string()
}
}
+42
View File
@@ -0,0 +1,42 @@
use std::fmt::{Debug, Formatter, Result as FmtResult};
use pyo3::pyclass;
use rosu_pp::model::mode::GameMode;
#[pyclass(eq, eq_int, name = "GameMode")]
#[derive(Copy, Clone, Default, PartialEq)]
pub enum PyGameMode {
#[default]
Osu,
Taiko,
Catch,
Mania,
}
impl Debug for PyGameMode {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(self.__pyo3__repr__())
}
}
impl From<PyGameMode> for GameMode {
fn from(mode: PyGameMode) -> Self {
match mode {
PyGameMode::Osu => Self::Osu,
PyGameMode::Taiko => Self::Taiko,
PyGameMode::Catch => Self::Catch,
PyGameMode::Mania => Self::Mania,
}
}
}
impl From<GameMode> for PyGameMode {
fn from(mode: GameMode) -> Self {
match mode {
GameMode::Osu => Self::Osu,
GameMode::Taiko => Self::Taiko,
GameMode::Catch => Self::Catch,
GameMode::Mania => Self::Mania,
}
}
}
+731
View File
@@ -0,0 +1,731 @@
use std::{
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult},
ops::Deref,
};
use pyo3::{
impl_::frompyobject::{
extract_struct_field, extract_tuple_struct_field, failed_to_extract_enum,
},
intern,
types::{
iter::BoundDictIterator, PyAnyMethods, PyDict, PyDictMethods, PyList, PyString,
PyStringMethods,
},
Bound, FromPyObject, PyAny, PyResult,
};
use rosu_mods::{serde::GameModSeed, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy};
use serde::de::{
value::{BorrowedStrDeserializer, CowStrDeserializer, MapAccessDeserializer, U32Deserializer},
DeserializeSeed, Deserializer, Error as DeError, MapAccess, Unexpected, Visitor,
};
use crate::error::ParseError;
#[derive(Clone)]
pub enum PyGameMods {
Lazer(GameModsLazer),
Intermode(GameModsIntermode),
Legacy(GameModsLegacy),
}
impl Default for PyGameMods {
fn default() -> Self {
Self::Legacy(GameModsLegacy::NoMod)
}
}
impl<'py> FromPyObject<'py> for PyGameMods {
fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
let errors = [
match extract_tuple_struct_field(obj, "PyInt", 0) {
Ok(bits) => return Ok(Self::Legacy(GameModsLegacy::from_bits(bits))),
Err(err) => err,
},
match extract_tuple_struct_field::<Bound<'py, PyString>>(obj, "PyString", 0) {
Ok(acronyms) => match acronyms.to_str() {
Ok(acronyms) => {
let intermode = GameModsIntermode::from_acronyms(acronyms);
let mods = match intermode.checked_bits() {
Some(bits) => Self::Legacy(GameModsLegacy::from_bits(bits)),
None => Self::Intermode(intermode),
};
return Ok(mods);
}
Err(err) => err,
},
Err(err) => err,
},
match extract_tuple_struct_field::<PyGameMod<'py>>(obj, "PyGameMod", 0) {
Ok(gamemod) => match GameModSeed::GuessMode.deserialize(gamemod) {
Ok(gamemod) => return Ok(Self::Lazer(gamemod.into())),
Err(DeserializeError(err)) => ParseError::new_err(err),
},
Err(err) => err,
},
match extract_tuple_struct_field::<Bound<'py, PyList>>(obj, "PyList", 0) {
Ok(list) => {
let seed = GameModSeed::GuessMode;
let res = list
.iter()?
.try_fold(GameModsLazer::new(), |mut mods, item| {
let res = match item?.extract::<PyGameModUnion<'_>>()? {
PyGameModUnion::Mod(gamemod) => seed.deserialize(gamemod),
PyGameModUnion::Acronym(acronym) => seed.deserialize(
CowStrDeserializer::new(acronym.to_string_lossy()),
),
PyGameModUnion::Bits(bits) => {
seed.deserialize(U32Deserializer::new(bits))
}
};
match res {
Ok(gamemod) => mods.insert(gamemod),
Err(DeserializeError(err)) => return Err(ParseError::new_err(err)),
}
Ok(mods)
});
match res {
Ok(mods) => return Ok(Self::Lazer(mods)),
Err(err) => err,
}
}
Err(err) => err,
},
];
Err(failed_to_extract_enum(
obj.py(),
"PyGameMods",
&["Legacy", "Intermode", "GameMods", "GameMods"],
&["int", "str", "GameMod", "List[GameMod | str | int]"],
&errors,
))
}
}
struct PyGameMod<'py> {
acronym: Bound<'py, PyString>,
settings: Option<Bound<'py, PyDict>>,
}
impl<'py> FromPyObject<'py> for PyGameMod<'py> {
fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
let py = obj.py();
let dict: Bound<'_, PyDict> = obj.extract()?;
// Force a `KeyError` if `acronym` is missing
let acronym = PyAnyMethods::get_item(dict.deref(), intern!(py, "acronym"))?;
let settings = dict.get_item(intern!(py, "settings"))?;
Ok(PyGameMod {
acronym: extract_struct_field(&acronym, "PyGameMod", "acronym")?,
settings: settings.as_ref().map(PyAnyMethods::extract).transpose()?,
})
}
}
#[derive(FromPyObject)]
enum PyGameModUnion<'py> {
Mod(PyGameMod<'py>),
Acronym(Bound<'py, PyString>),
Bits(u32),
}
#[derive(Debug)]
struct DeserializeError(String);
impl DeError for DeserializeError {
fn custom<T: Display>(msg: T) -> Self {
Self(msg.to_string())
}
}
impl Display for DeserializeError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(&self.0)
}
}
impl StdError for DeserializeError {}
impl<'de> Deserializer<'de> for PyGameMod<'de> {
type Error = DeserializeError;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_map(visitor)
}
fn deserialize_bool<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i8<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i16<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i32<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u8<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u16<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u32<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_f32<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_f64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_char<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_str<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_string<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_bytes<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_byte_buf<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_option<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_unit<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_unit_struct<V>(self, _: &'static str, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_newtype_struct<V>(self, _: &'static str, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_seq<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_tuple<V>(self, _: usize, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_tuple_struct<V>(
self,
_: &'static str,
_: usize,
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_map(PyGameModMap::Full(self))
}
fn deserialize_struct<V>(
self,
_: &'static str,
_: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_enum<V>(
self,
_: &'static str,
_: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_identifier<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_ignored_any<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
}
enum PyGameModMap<'py> {
Full(PyGameMod<'py>),
Settings(Bound<'py, PyDict>),
Done,
}
impl<'de> MapAccess<'de> for PyGameModMap<'de> {
type Error = DeserializeError;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
let key = match self {
PyGameModMap::Full(_) => "acronym",
PyGameModMap::Settings(_) => "settings",
PyGameModMap::Done => return Ok(None),
};
seed.deserialize(BorrowedStrDeserializer::new(key))
.map(Some)
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
match self {
PyGameModMap::Full(gamemod) => {
let acronym = gamemod.acronym.to_string_lossy();
let res = seed.deserialize(CowStrDeserializer::new(acronym));
*self = gamemod.settings.take().map_or(Self::Done, Self::Settings);
res
}
PyGameModMap::Settings(dict) => {
let access = DictAccess {
iter: dict.iter(),
next_value: None,
};
let res = seed.deserialize(MapAccessDeserializer::new(access));
*self = Self::Done;
res
}
PyGameModMap::Done => unimplemented!(),
}
}
}
struct DictAccess<'py> {
iter: BoundDictIterator<'py>,
next_value: Option<PyValue<'py>>,
}
impl<'de> MapAccess<'de> for DictAccess<'de> {
type Error = DeserializeError;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
debug_assert!(self.next_value.is_none());
match self.iter.next() {
Some((key, value)) => {
let key: Bound<'_, PyString> = key.extract().map_err(DeError::custom)?;
let value: PyValue<'_> = value.extract().map_err(DeError::custom)?;
self.next_value = Some(value);
seed.deserialize(PyValue::String(key)).map(Some)
}
None => Ok(None),
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
seed.deserialize(self.next_value.take().unwrap())
}
fn size_hint(&self) -> Option<usize> {
Some(self.iter.len())
}
}
#[derive(FromPyObject)]
enum PyValue<'py> {
Bool(bool),
Number(f32),
String(Bound<'py, PyString>),
}
impl<'de> Deserializer<'de> for PyValue<'de> {
type Error = DeserializeError;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
PyValue::Bool(v) => visitor.visit_bool(v),
PyValue::Number(v) => visitor.visit_f32(v),
PyValue::String(v) => visitor.visit_string(v.to_string_lossy().into_owned()),
}
}
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
Self::Bool(v) => visitor.visit_bool(v),
Self::Number(v) => Err(DeError::invalid_type(
Unexpected::Float(f64::from(v)),
&visitor,
)),
Self::String(v) => Err(DeError::invalid_type(
Unexpected::Str(v.to_string_lossy().as_ref()),
&visitor,
)),
}
}
fn deserialize_i8<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i16<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i32<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_i64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u8<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u16<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u32<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_u64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
Self::Bool(v) => Err(DeError::invalid_type(Unexpected::Bool(v), &visitor)),
Self::Number(v) => visitor.visit_f32(v),
Self::String(v) => Err(DeError::invalid_type(
Unexpected::Str(v.to_string_lossy().as_ref()),
&visitor,
)),
}
}
fn deserialize_f64<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_char<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
Self::Bool(v) => Err(DeError::invalid_type(Unexpected::Bool(v), &visitor)),
Self::Number(v) => Err(DeError::invalid_type(
Unexpected::Float(f64::from(v)),
&visitor,
)),
Self::String(v) => visitor.visit_str(v.to_string_lossy().as_ref()),
}
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self {
Self::Bool(v) => Err(DeError::invalid_type(Unexpected::Bool(v), &visitor)),
Self::Number(v) => Err(DeError::invalid_type(
Unexpected::Float(f64::from(v)),
&visitor,
)),
Self::String(v) => visitor.visit_string(v.to_string_lossy().into_owned()),
}
}
fn deserialize_bytes<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_byte_buf<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_some(self)
}
fn deserialize_unit<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_unit_struct<V>(self, _: &'static str, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_newtype_struct<V>(self, _: &'static str, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_seq<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_tuple<V>(self, _: usize, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_tuple_struct<V>(
self,
_: &'static str,
_: usize,
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_map<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_struct<V>(
self,
_: &'static str,
_: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_enum<V>(
self,
_: &'static str,
_: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_identifier<V>(self, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
unimplemented!()
}
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
}
-145
View File
@@ -1,145 +0,0 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use akatsuki_pp::{
catch::CatchPerformanceAttributes, mania::ManiaPerformanceAttributes,
osu::OsuPerformanceAttributes, taiko::TaikoPerformanceAttributes, PerformanceAttributes,
};
use crate::diff_attrs::{BorrowedDifficultyAttributes, PyDifficultyAttributes};
#[pyclass(name = "PerformanceAttributes")]
#[derive(Debug)]
pub struct PyPerformanceAttributes {
inner: PerformanceAttributes,
}
impl From<PerformanceAttributes> for PyPerformanceAttributes {
#[inline]
fn from(attrs: PerformanceAttributes) -> Self {
Self { inner: attrs }
}
}
impl Display for PyPerformanceAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("PerformanceAttributes");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match &self.inner {
PerformanceAttributes::Osu(attrs) => {
let OsuPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
effective_miss_count,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &0_u8).field("difficulty", &borrowed);
debug! {
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
effective_miss_count,
};
}
PerformanceAttributes::Taiko(attrs) => {
let TaikoPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_difficulty,
effective_miss_count,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &1_u8).field("difficulty", &borrowed);
debug!(pp, pp_acc, pp_difficulty, effective_miss_count);
}
PerformanceAttributes::Catch(attrs) => {
let CatchPerformanceAttributes { difficulty, pp } = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &2_u8).field("difficulty", &borrowed);
debug!(pp);
}
PerformanceAttributes::Mania(attrs) => {
let ManiaPerformanceAttributes {
difficulty,
pp,
pp_difficulty,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &3_u8).field("difficulty", &borrowed);
debug!(pp, pp_difficulty);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$field:ident: ( $( $mode:ident ),* ),
)*
) => {
#[pymethods]
impl PyPerformanceAttributes {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
PerformanceAttributes::Osu(_) => 0,
PerformanceAttributes::Taiko(_) => 1,
PerformanceAttributes::Catch(_) => 2,
PerformanceAttributes::Mania(_) => 3,
}
}
#[getter]
fn difficulty(&self) -> PyDifficultyAttributes {
self.inner.difficulty_attributes().into()
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
#[getter]
fn $field(&self) -> Option<f64> {
match &self.inner {
$( PerformanceAttributes::$mode(attrs) => Some(attrs.$field), )*
#[allow(unreachable_patterns)]
_ => None,
}
}
)*
}
};
}
getters! {
pp: (Osu, Taiko, Catch, Mania),
pp_aim: (Osu),
pp_flashlight: (Osu),
pp_speed: (Osu),
pp_acc: (Osu, Taiko),
effective_miss_count: (Osu, Taiko),
pp_difficulty: (Taiko, Mania),
}
+450
View File
@@ -0,0 +1,450 @@
use pyo3::{
exceptions::PyTypeError,
pyclass, pymethods,
types::{PyAnyMethods, PyDict},
Bound, PyAny, PyRef, PyResult,
};
use rosu_pp::{
any::{DifficultyAttributes, HitResultPriority},
Difficulty, Performance,
};
use crate::{
attributes::{difficulty::PyDifficultyAttributes, performance::PyPerformanceAttributes},
beatmap::PyBeatmap,
difficulty::PyDifficulty,
error::ArgsError,
mods::PyGameMods,
};
#[pyclass(name = "Performance")]
#[derive(Default)]
pub struct PyPerformance {
pub(crate) mods: PyGameMods,
pub(crate) clock_rate: Option<f64>,
pub(crate) ar: Option<f32>,
pub(crate) ar_with_mods: bool,
pub(crate) cs: Option<f32>,
pub(crate) cs_with_mods: bool,
pub(crate) hp: Option<f32>,
pub(crate) hp_with_mods: bool,
pub(crate) od: Option<f32>,
pub(crate) od_with_mods: bool,
pub(crate) passed_objects: Option<u32>,
pub(crate) hardrock_offsets: Option<bool>,
pub(crate) accuracy: Option<f64>,
pub(crate) combo: Option<u32>,
pub(crate) n_geki: Option<u32>,
pub(crate) n_katu: Option<u32>,
pub(crate) n300: Option<u32>,
pub(crate) n100: Option<u32>,
pub(crate) n50: Option<u32>,
pub(crate) misses: Option<u32>,
pub(crate) hitresult_priority: PyHitResultPriority,
}
#[pymethods]
impl PyPerformance {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs {
match key.extract()? {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be GameMods"))?
}
"clock_rate" => {
this.clock_rate =
Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'clock_rate': must be a float")
})?)
}
"ar" => {
this.ar = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar': must be a float"))?,
)
}
"ar_with_mods" => {
this.ar_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar_with_mods': must be a bool"))?
}
"cs" => {
this.cs = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs': must be a float"))?,
)
}
"cs_with_mods" => {
this.cs_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'cs_with_mods': must be a bool"))?
}
"hp" => {
this.hp = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp': must be a float"))?,
)
}
"hp_with_mods" => {
this.hp_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'hp_with_mods': must be a bool"))?
}
"od" => {
this.od = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od': must be a float"))?,
)
}
"od_with_mods" => {
this.od_with_mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'od_with_mods': must be a bool"))?
}
"passed_objects" => {
this.passed_objects = value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'passed_objects': must be an int")
})?
}
"hardrock_offsets" => {
this.hardrock_offsets = value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'hardrock_offsets': must be a bool")
})?
}
"accuracy" => {
this.accuracy =
Some(value.extract().map_err(|_| {
PyTypeError::new_err("kwarg 'accuracy': must be a float")
})?)
}
"combo" => {
this.combo = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'combo': must be an int"))?,
)
}
"n_geki" => {
this.n_geki = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_geki': must be an int"))?,
)
}
"n_katu" => {
this.n_katu = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_katu': must be an int"))?,
)
}
"n300" => {
this.n300 = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n300': must be an int"))?,
)
}
"n100" => {
this.n100 = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n100': must be an int"))?,
)
}
"n50" => {
this.n50 = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n50': must be an int"))?,
)
}
"misses" => {
this.misses = Some(
value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'misses': must be an int"))?,
)
}
"hitresult_priority" => {
this.hitresult_priority = value.extract().map_err(|_| {
PyTypeError::new_err(
"kwarg 'hitresult_priority': must be a HitResultPriority",
)
})?;
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'mods', \n\
'clock_rate', 'ar', 'ar_with_mods', 'cs', \n\
'cs_with_mods', 'hp', 'hp_with_mods', 'od', \n\
'od_with_mods', 'passed_objects', 'hardrock_offsets', \n\
'accuracy', 'combo', 'n_geki', 'n_katu', 'n300', 'n100', \n\
'n50', 'misses', or 'hitresult_priority'"
);
return Err(ArgsError::new_err(err));
}
}
}
Ok(this)
}
fn calculate(&self, args: &Bound<'_, PyAny>) -> PyResult<PyPerformanceAttributes> {
let map;
let mut perf = if let Ok(attrs) = args.extract::<PyPerformanceAttributes>() {
Performance::new(DifficultyAttributes::try_from(attrs.difficulty)?)
} else if let Ok(attrs) = args.extract::<PyDifficultyAttributes>() {
Performance::new(DifficultyAttributes::try_from(attrs)?)
} else if let Ok(map_) = args.extract::<PyRef<'_, PyBeatmap>>() {
map = map_;
Performance::new(&map.inner)
} else {
return Err(ArgsError::new_err(
"argument must be DifficultyAttributes, PerformanceAttributes, or a Beatmap",
));
};
perf = self.apply(perf);
let state = perf.generate_state();
let mut attrs = PyPerformanceAttributes::from(perf.calculate());
attrs.state = Some(state.into());
Ok(attrs)
}
fn difficulty(&self) -> PyDifficulty {
let Self {
mods,
clock_rate,
ar,
ar_with_mods,
cs,
cs_with_mods,
hp,
hp_with_mods,
od,
od_with_mods,
passed_objects,
hardrock_offsets,
..
} = self;
PyDifficulty {
mods: mods.clone(),
clock_rate: *clock_rate,
ar: *ar,
ar_with_mods: *ar_with_mods,
cs: *cs,
cs_with_mods: *cs_with_mods,
hp: *hp,
hp_with_mods: *hp_with_mods,
od: *od,
od_with_mods: *od_with_mods,
passed_objects: *passed_objects,
hardrock_offsets: *hardrock_offsets,
}
}
#[pyo3(signature = (mods=None))]
fn set_mods(&mut self, mods: Option<PyGameMods>) {
self.mods = mods.unwrap_or_default();
}
#[pyo3(signature = (clock_rate=None))]
fn set_clock_rate(&mut self, clock_rate: Option<f64>) {
self.clock_rate = clock_rate;
}
#[pyo3(signature = (ar, ar_with_mods))]
fn set_ar(&mut self, ar: Option<f32>, ar_with_mods: bool) {
self.ar = ar;
self.ar_with_mods = ar_with_mods;
}
#[pyo3(signature = (cs, cs_with_mods))]
fn set_cs(&mut self, cs: Option<f32>, cs_with_mods: bool) {
self.cs = cs;
self.cs_with_mods = cs_with_mods;
}
#[pyo3(signature = (hp, hp_with_mods))]
fn set_hp(&mut self, hp: Option<f32>, hp_with_mods: bool) {
self.hp = hp;
self.hp_with_mods = hp_with_mods;
}
#[pyo3(signature = (od, od_with_mods))]
fn set_od(&mut self, od: Option<f32>, od_with_mods: bool) {
self.od = od;
self.od_with_mods = od_with_mods;
}
#[pyo3(signature = (passed_objects=None))]
fn set_passed_objects(&mut self, passed_objects: Option<u32>) {
self.passed_objects = passed_objects;
}
#[pyo3(signature = (hardrock_offsets=None))]
fn set_hardrock_offsets(&mut self, hardrock_offsets: Option<bool>) {
self.hardrock_offsets = hardrock_offsets;
}
#[pyo3(signature = (accuracy=None))]
fn set_accuracy(&mut self, accuracy: Option<f64>) {
self.accuracy = accuracy;
}
#[pyo3(signature = (combo=None))]
fn set_combo(&mut self, combo: Option<u32>) {
self.combo = combo;
}
#[pyo3(signature = (n_geki=None))]
fn set_n_geki(&mut self, n_geki: Option<u32>) {
self.n_geki = n_geki;
}
#[pyo3(signature = (n_katu=None))]
fn set_n_katu(&mut self, n_katu: Option<u32>) {
self.n_katu = n_katu;
}
#[pyo3(signature = (n300=None))]
fn set_n300(&mut self, n300: Option<u32>) {
self.n300 = n300;
}
#[pyo3(signature = (n100=None))]
fn set_n100(&mut self, n100: Option<u32>) {
self.n100 = n100;
}
#[pyo3(signature = (n50=None))]
fn set_n50(&mut self, n50: Option<u32>) {
self.n50 = n50;
}
#[pyo3(signature = (misses=None))]
fn set_misses(&mut self, misses: Option<u32>) {
self.misses = misses;
}
#[pyo3(signature = (hitresult_priority=None))]
fn set_hitresult_priority(&mut self, hitresult_priority: Option<PyHitResultPriority>) {
self.hitresult_priority = hitresult_priority.unwrap_or_default();
}
}
impl PyPerformance {
fn apply<'a>(&self, mut perf: Performance<'a>) -> Performance<'a> {
if let Some(accuracy) = self.accuracy {
perf = perf.accuracy(accuracy);
}
if let Some(combo) = self.combo {
perf = perf.combo(combo);
}
if let Some(n_geki) = self.n_geki {
perf = perf.n_geki(n_geki);
}
if let Some(n_katu) = self.n_katu {
perf = perf.n_katu(n_katu);
}
if let Some(n300) = self.n300 {
perf = perf.n300(n300);
}
if let Some(n100) = self.n100 {
perf = perf.n100(n100);
}
if let Some(n50) = self.n50 {
perf = perf.n50(n50);
}
if let Some(misses) = self.misses {
perf = perf.misses(misses);
}
perf.hitresult_priority(self.hitresult_priority.into())
.difficulty(self.as_difficulty())
}
fn as_difficulty(&self) -> Difficulty {
let mut difficulty = Difficulty::new();
difficulty = match self.mods {
PyGameMods::Lazer(ref mods) => difficulty.mods(mods.clone()),
PyGameMods::Intermode(ref mods) => difficulty.mods(mods),
PyGameMods::Legacy(mods) => difficulty.mods(mods),
};
if let Some(passed_objects) = self.passed_objects {
difficulty = difficulty.passed_objects(passed_objects);
}
if let Some(clock_rate) = self.clock_rate {
difficulty = difficulty.clock_rate(clock_rate);
}
if let Some(ar) = self.ar {
difficulty = difficulty.ar(ar, self.ar_with_mods);
}
if let Some(cs) = self.cs {
difficulty = difficulty.cs(cs, self.cs_with_mods);
}
if let Some(hp) = self.hp {
difficulty = difficulty.hp(hp, self.hp_with_mods);
}
if let Some(od) = self.od {
difficulty = difficulty.od(od, self.od_with_mods);
}
if let Some(hardrock_offsets) = self.hardrock_offsets {
difficulty = difficulty.hardrock_offsets(hardrock_offsets);
}
difficulty
}
}
#[pyclass(eq, eq_int, name = "HitResultPriority")]
#[derive(Copy, Clone, Default, PartialEq)]
pub enum PyHitResultPriority {
#[default]
BestCase,
WorstCase,
}
impl From<PyHitResultPriority> for HitResultPriority {
fn from(priority: PyHitResultPriority) -> Self {
match priority {
PyHitResultPriority::BestCase => Self::BestCase,
PyHitResultPriority::WorstCase => Self::WorstCase,
}
}
}
+155
View File
@@ -0,0 +1,155 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{
exceptions::PyTypeError,
pyclass,
types::{PyAnyMethods, PyDict},
Bound, PyResult,
};
use rosu_pp::any::ScoreState;
use crate::error::ArgsError;
#[pyclass(name = "ScoreState")]
#[derive(Clone, Default)]
pub struct PyScoreState {
#[pyo3(get, set)]
max_combo: u32,
#[pyo3(get, set)]
n_geki: u32,
#[pyo3(get, set)]
n_katu: u32,
#[pyo3(get, set)]
n300: u32,
#[pyo3(get, set)]
n100: u32,
#[pyo3(get, set)]
n50: u32,
#[pyo3(get, set)]
misses: u32,
}
#[pyo3::pymethods]
impl PyScoreState {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs {
match key.extract()? {
"max_combo" => {
this.max_combo = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'max_combo': must be an int"))?
}
"n_geki" => {
this.n_geki = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_geki': must be an int"))?
}
"n_katu" => {
this.n_katu = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n_katu': must be an int"))?
}
"n300" => {
this.n300 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n300': must be an int"))?
}
"n100" => {
this.n100 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n100': must be an int"))?
}
"n50" => {
this.n50 = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'n50': must be an int"))?
}
"misses" => {
this.misses = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'misses': must be an int"))?
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'max_combo', \n\
'n_geki', 'n_katu', 'n300', 'n100', 'n50' or 'misses'",
);
return Err(ArgsError::new_err(err));
}
}
}
Ok(this)
}
fn __repr__(&self) -> String {
self.to_string()
}
}
impl Debug for PyScoreState {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let Self {
max_combo,
n_geki,
n_katu,
n300,
n100,
n50,
misses,
} = self;
f.debug_struct("ScoreState")
.field("max_combo", max_combo)
.field("n_geki", n_geki)
.field("n_katu", n_katu)
.field("n300", n300)
.field("n100", n100)
.field("n50", n50)
.field("misses", misses)
.finish()
}
}
impl Display for PyScoreState {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
Debug::fmt(self, f)
}
}
impl From<&PyScoreState> for ScoreState {
fn from(state: &PyScoreState) -> Self {
Self {
max_combo: state.max_combo,
n_geki: state.n_geki,
n_katu: state.n_katu,
n300: state.n300,
n100: state.n100,
n50: state.n50,
misses: state.misses,
}
}
}
impl From<ScoreState> for PyScoreState {
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n_geki: state.n_geki,
n_katu: state.n_katu,
n300: state.n300,
n100: state.n100,
n50: state.n50,
misses: state.misses,
}
}
}
+94 -165
View File
@@ -1,174 +1,103 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{ffi, pyclass, pymethods, types::PyList, IntoPy, IntoPyPointer, Py, PyObject, Python};
use akatsuki_pp::{
catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains, Strains,
use pyo3::pyclass;
use rosu_pp::{
any::Strains, catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains,
};
#[pyclass(name = "Strains")]
#[derive(Debug)]
pub struct PyStrains {
inner: Strains,
use crate::mode::PyGameMode;
type DoubleList = Vec<f64>;
define_class! {
#[pyclass(name = "Strains", frozen)]
#[derive(Default)]
pub struct PyStrains {
pub mode: PyGameMode!,
pub section_length: f64!,
pub aim: DoubleList?,
pub aim_no_sliders: DoubleList?,
pub speed: DoubleList?,
pub flashlight: DoubleList?,
pub color: DoubleList?,
pub rhythm: DoubleList?,
pub stamina: DoubleList?,
pub movement: DoubleList?,
pub strains: DoubleList?,
}
}
impl From<OsuStrains> for PyStrains {
fn from(strains: OsuStrains) -> Self {
let OsuStrains {
aim,
aim_no_sliders,
speed,
flashlight,
} = strains;
Self {
mode: PyGameMode::Osu,
section_length: OsuStrains::SECTION_LEN,
aim: Some(aim),
aim_no_sliders: Some(aim_no_sliders),
speed: Some(speed),
flashlight: Some(flashlight),
..Self::default()
}
}
}
impl From<TaikoStrains> for PyStrains {
fn from(strains: TaikoStrains) -> Self {
let TaikoStrains {
color,
rhythm,
stamina,
} = strains;
Self {
mode: PyGameMode::Taiko,
section_length: TaikoStrains::SECTION_LEN,
color: Some(color),
rhythm: Some(rhythm),
stamina: Some(stamina),
..Self::default()
}
}
}
impl From<CatchStrains> for PyStrains {
fn from(strains: CatchStrains) -> Self {
let CatchStrains { movement } = strains;
Self {
mode: PyGameMode::Catch,
section_length: CatchStrains::SECTION_LEN,
movement: Some(movement),
..Self::default()
}
}
}
impl From<ManiaStrains> for PyStrains {
fn from(strains: ManiaStrains) -> Self {
let ManiaStrains { strains } = strains;
Self {
mode: PyGameMode::Mania,
section_length: ManiaStrains::SECTION_LEN,
strains: Some(strains),
..Self::default()
}
}
}
impl From<Strains> for PyStrains {
#[inline]
fn from(strains: Strains) -> Self {
Self { inner: strains }
}
}
impl Display for PyStrains {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("Strains");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match &self.inner {
Strains::Osu(strains) => {
let OsuStrains {
section_len,
aim,
aim_no_sliders,
speed,
flashlight,
} = strains;
debug.field("mode", &0_u8);
debug!(section_len, aim, aim_no_sliders, speed, flashlight);
}
Strains::Taiko(strains) => {
let TaikoStrains {
section_len,
color,
rhythm,
stamina,
} = strains;
debug.field("mode", &1_u8);
debug!(section_len, color, rhythm, stamina);
}
Strains::Catch(strains) => {
let CatchStrains {
section_len,
movement,
} = strains;
debug.field("mode", &2_u8);
debug!(section_len, movement);
}
Strains::Mania(strains) => {
let ManiaStrains {
section_len,
strains,
} = strains;
debug.field("mode", &3_u8);
debug!(section_len, strains);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$mode:ident {
$( $field:ident ,)*
},
)*
) => {
#[pymethods]
impl PyStrains {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
Strains::Osu(_) => 0,
Strains::Taiko(_) => 1,
Strains::Catch(_) => 2,
Strains::Mania(_) => 3,
}
}
#[getter]
fn section_len(&self) -> f64 {
match &self.inner {
Strains::Osu(strains) => strains.section_len,
Strains::Taiko(strains) => strains.section_len,
Strains::Catch(strains) => strains.section_len,
Strains::Mania(strains) => strains.section_len,
}
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
$(
#[getter]
fn $field(&self) -> Option<SliceWrapper<'_>> {
if let Strains::$mode(ref attrs) = self.inner {
Some(SliceWrapper(&attrs.$field))
} else {
None
}
}
)*
)*
}
};
}
getters! {
Osu {
aim,
aim_no_sliders,
speed,
flashlight,
},
Taiko {
color,
stamina,
rhythm,
},
Catch {
movement,
},
Mania {
strains,
},
}
struct SliceWrapper<'i>(&'i [f64]);
impl IntoPy<PyObject> for SliceWrapper<'_> {
#[inline]
fn into_py(self, py: Python<'_>) -> PyObject {
let iter = self.0.iter().map(|e| e.into_py(py));
let len = self.0.len() as ffi::Py_ssize_t;
// SAFETY: analogous code to pyo3's `IntoPy` impl for `Vec<T>`
// https://github.com/PyO3/pyo3/blob/d7b05cbcf5785019a097e496454b924f3e11d94f/src/types/list.rs#L21-L54
unsafe {
let ptr = ffi::PyList_New(len);
let list: Py<PyList> = Py::from_owned_ptr(py, ptr);
for (item, i) in iter.zip(0..) {
#[cfg(not(Py_LIMITED_API))]
ffi::PyList_SET_ITEM(ptr, i, item.into_ptr());
#[cfg(Py_LIMITED_API)]
ffi::PyList_SetItem(ptr, i, obj.into_ptr());
}
list.into()
match strains {
Strains::Osu(strains) => strains.into(),
Strains::Taiko(strains) => strains.into(),
Strains::Catch(strains) => strains.into(),
Strains::Mania(strains) => strains.into(),
}
}
}