Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ed5a770a | |||
| ecec26fda5 | |||
| bf6e57a520 | |||
| 4d29dd752b | |||
| 5ed4ce3414 | |||
| fc60284511 | |||
| b94dbf06d7 | |||
| 0c2aa11ccd | |||
| 4a8b0a9a1b | |||
| ab03b988e2 | |||
| f0f08fe9c9 | |||
| cb4b965eae | |||
| 3c7f6533fe | |||
| e2fb4cc342 | |||
| c5fdf695d7 | |||
| b1fe7f33b2 | |||
| bcad53579a | |||
| 5d68891f66 | |||
| fa981c4769 | |||
| 0bbd18648e | |||
| 197d687b98 | |||
| 0ce07c0e6c | |||
| 757b807f54 | |||
| f7c41a516f | |||
| c0e1977a54 | |||
| 2a37169ca8 | |||
| 583ce7ef0e | |||
| 361002989f | |||
| a8e521ef8c | |||
| 4fd090d625 | |||
| ce30124c5e | |||
| 45e27f71e9 | |||
| 84a37b904e | |||
| bffc06c17e | |||
| 4fe3395457 | |||
| 493d836f96 | |||
| 984b899088 | |||
| 37471f18ec | |||
| 30d1502bdc | |||
| 4809e599dc | |||
| 8ab467b2c7 | |||
| 92cf2e9bfc | |||
| 488ad77840 | |||
| 2d18ef664f |
@@ -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 }}
|
||||
@@ -1,150 +0,0 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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 *
|
||||
@@ -1,4 +1,6 @@
|
||||
/target
|
||||
expanded.rs
|
||||
output.rs
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
+39
-1
@@ -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
|
||||
|
||||
Generated
+106
-166
@@ -2,31 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "akatsuki-pp"
|
||||
version = "0.9.4"
|
||||
source = "git+https://github.com/osuAkatsuki/akatsuki-pp-rs?rev=764da0dbf42ed80441acf99d070aaedabaed4b75#764da0dbf42ed80441acf99d070aaedabaed4b75"
|
||||
|
||||
[[package]]
|
||||
name = "akatsuki-pp-py"
|
||||
version = "0.9.4"
|
||||
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
@@ -1,20 +1,25 @@
|
||||
[package]
|
||||
name = "akatsuki-pp-py"
|
||||
version = "0.9.7"
|
||||
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 = "764da0dbf42ed80441acf99d070aaedabaed4b75" }
|
||||
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
|
||||
|
||||
@@ -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/
|
||||
@@ -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
-8
@@ -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 = "0.9.7"
|
||||
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,9 +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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod beatmap;
|
||||
pub mod difficulty;
|
||||
pub mod performance;
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,334 +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,
|
||||
},
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
|
||||
use pyo3::{pyclass, pymethods};
|
||||
use akatsuki_pp::{
|
||||
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
|
||||
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes, DifficultyAttributes,
|
||||
};
|
||||
|
||||
#[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,
|
||||
} = 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,
|
||||
}
|
||||
}
|
||||
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),
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod difficulty;
|
||||
pub mod performance;
|
||||
@@ -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
-16
@@ -1,37 +1,59 @@
|
||||
#![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(())
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
-164
@@ -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)]
|
||||
use crate::mode::PyGameMode;
|
||||
|
||||
type DoubleList = Vec<f64>;
|
||||
|
||||
define_class! {
|
||||
#[pyclass(name = "Strains", frozen)]
|
||||
#[derive(Default)]
|
||||
pub struct PyStrains {
|
||||
inner: Strains,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user