initial commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: messense/maturin-action@v1
|
||||
with:
|
||||
manylinux: auto
|
||||
command: build
|
||||
args: --release -o dist
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: messense/maturin-action@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --no-sdist -o dist
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: messense/maturin-action@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --no-sdist -o dist --universal2
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
if: "startsWith(github.ref, 'refs/tags/')"
|
||||
needs: [ macos, windows, linux ]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: wheels
|
||||
- name: Publish to PyPI
|
||||
uses: messense/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
with:
|
||||
command: upload
|
||||
args: --skip-existing *
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/target
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.venv/
|
||||
env/
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
include/
|
||||
man/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
pip-selfcheck.json
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.pot
|
||||
|
||||
.DS_Store
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Pyenv
|
||||
.python-version
|
||||
Generated
+264
@@ -0,0 +1,264 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8"
|
||||
dependencies = [
|
||||
"indoc-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880"
|
||||
dependencies = [
|
||||
"paste-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste-impl"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "392a54546fda6b7cc663379d0e6ce8b324cf88aecc5a499838e1be9781bdce2e"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cf01dbf1c05af0a14c7779ed6f3aa9deac9c3419606ac9de537a2d649005720"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pyo3-build-config",
|
||||
"pyo3-macros",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbf9e4d128bfbddc898ad3409900080d8d5095c379632fbbfbb9c8cfb1fb852b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67701eb32b1f9a9722b4bc54b548ff9d7ebfded011c12daece7b9063be1fd755"
|
||||
dependencies = [
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f44f09e825ee49a105f2c7b23ebee50886a9aee0746f4dd5a704138a64b0218a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rosu-pp"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4efb6f419a910e96683aada6e12c14f4a4b1da286f1a205a1edc3eca3b8fa69e"
|
||||
|
||||
[[package]]
|
||||
name = "rosu-pp-py"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
"rosu-pp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "rosu-pp-py"
|
||||
version = "0.4.0"
|
||||
description = "osu! difficulty and pp calculation for all modes"
|
||||
authors = ["Max Ohn <ohn.m@hotmail.de>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "rosu_pp_py"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
pyo3 = { version = "0.15.1", features = ["macros", "extension-module"] }
|
||||
rosu-pp = { version = "0.4" }
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Max
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,136 @@
|
||||
# rosu-pp-py
|
||||
|
||||
Difficulty and performance calculation for all [osu!](https://osu.ppy.sh/) 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 for more info.
|
||||
|
||||
## How to use rosu-pp-py
|
||||
|
||||
The library exposes three classes: `Calculator`, `ScoreParams`, and `CalculateResult`.
|
||||
|
||||
1) The first step is to create a new `Calculator` instance by providing the constructor the path to a `.osu` beatmap file like so
|
||||
```py
|
||||
calculator = Calculator('/path/to/file.osu')
|
||||
```
|
||||
2) Next, you need to create `ScoreParams`. It has the following fields:
|
||||
```
|
||||
- mods: Optional[int], // bit value for mods, defaults to 0 (NM) see https://github.com/ppy/osu-api/wiki#mods
|
||||
- acc: Optional[float], // if neither acc nor hitresults are specified, acc defaults to 100.0
|
||||
- n300: Optional[int], // defaults to value based on acc
|
||||
- n100: Optional[int], // defaults to value based on acc
|
||||
- n50: Optional[int], // defaults to value based on acc
|
||||
- nMisses: Optional[int], // defaults to 0
|
||||
- nKatu: Optional[int], // only relevant for osu!ctb
|
||||
- combo: Optional[int], // defaults to full combo
|
||||
- score: Optional[int], // only relevant for osu!mania
|
||||
- passedObjects: Optional[int], // only consider this many hit objects; useful for failed scores; defaults to all objects
|
||||
```
|
||||
Note that all fields are optional. If nothing is specified, the parameters are equivalent to the parameters of the best possible NM score.
|
||||
`ScoreParams` can be created either by calling the constructor without arguments and then set the fields manually like so
|
||||
```py
|
||||
params = ScoreParams()
|
||||
params.acc = 98.76
|
||||
```
|
||||
or they can be created by passing kwargs to the constructor directly like so
|
||||
```py
|
||||
params = ScoreParams(acc = 98.76)
|
||||
```
|
||||
3) The last step is to provide the `ScoreParams` to the `Calculator` through the function `calculate`. This function takes one argument which must be either a single `ScoreParams` or an `Iterable[ScoreParams]`, i.e. anything that python can iterate over like a list, set, ...
|
||||
|
||||
## Example
|
||||
|
||||
```py
|
||||
from rosu_pp_py import Calculator, ScoreParams
|
||||
|
||||
calculator = Calculator('./maps/1980365.osu')
|
||||
|
||||
params1 = ScoreParams(
|
||||
mods = 8 + 16, # HDHR
|
||||
acc = 97.89,
|
||||
nMisses = 13,
|
||||
combo = 1388,
|
||||
)
|
||||
params2 = ScoreParams(mods = 24)
|
||||
|
||||
[result] = calculator.calculate(params1)
|
||||
results = calculator.calculate([params1, params2])
|
||||
assert result == results[0]
|
||||
print(f'PP: {results[0].pp}/{results[1].pp} | Stars: {results[1].stars}')
|
||||
```
|
||||
|
||||
## Return object structure
|
||||
|
||||
The `Calculator::calculate` function will provide you a **list of `CalculateResult`**, one for each score you specified parameters for. `CalculateResult` contains the difficulty and performance attributes. Most of its attributes are optional based on the map's mode. In the following, O/T/C/M will denote for which mode the given attribute will be present:
|
||||
|
||||
```
|
||||
mode: int
|
||||
Gamemode of the map, 0=O, 1=T, 2=C, 3=M. (O/T/C/M)
|
||||
stars: float
|
||||
Star rating of the map. (O/T/C/M)
|
||||
pp: float
|
||||
Performance points of the score. (O/T/C/M)
|
||||
ppAcc: Optional[float]
|
||||
Accuracy based portion of the performance points. (O/T/M)
|
||||
ppAim: Optional[float]
|
||||
Aim based portion of the performance points. (O)
|
||||
ppFlashlight: Optional[float]
|
||||
Flashlight based portion of the performance points. (O)
|
||||
ppSpeed: Optional[float]
|
||||
Speed based portion of the performance points. (O)
|
||||
ppStrain: Optional[float]
|
||||
Strain based portion of the performance points. (T/M)
|
||||
nFruits: Optional[int]
|
||||
The amount of fruits in the map. (C)
|
||||
nDroplets: Optional[int]
|
||||
The amount of droplets in the map. (C)
|
||||
nTinyDroplets: Optional[int]
|
||||
The amount of tiny droplets in the map. (C)
|
||||
aimStrain: Optional[float]
|
||||
Aim based portion of the star rating. (O)
|
||||
speedStrain: Optional[float]
|
||||
Speed based portion of the star rating. (O)
|
||||
flashlightRating: Optional[float]
|
||||
Flashlight based portion of the star rating. (O)
|
||||
sliderFactor: Optional[float]
|
||||
Nerf factor for sliders. (O)
|
||||
ar: float
|
||||
Approach rate of the map. (O/T/C/M)
|
||||
cs: float
|
||||
Circle size of the map. (O/T/C/M)
|
||||
hp: float
|
||||
Health drain rate of the map. (O/T/C/M)
|
||||
od: float
|
||||
Overall difficulty of the map. (O/T/C/M)
|
||||
bpm: float
|
||||
Beats per minute of the map. (O/T/C/M)
|
||||
nCircles: Optional[int]
|
||||
The amount of circles in the map. (O/T/M)
|
||||
nSliders: Optional[int]
|
||||
The amount of sliders in the map. (O/T/M)
|
||||
nSpinners: Optional[int]
|
||||
The amount of spinners in the map. (O/T/C)
|
||||
maxCombo: Optional[int]
|
||||
The max combo of the map. (O/T/C)
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```sh
|
||||
$ pip install rosu-pp-py
|
||||
```
|
||||
or
|
||||
```
|
||||
$ pip install https://github.com/MaxOhn/rosu-pp-py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
- [rosu-pp documentation](https://docs.rs/rosu-pp/latest/rosu_pp/)
|
||||
- [Rust documentation](https://www.rust-lang.org).
|
||||
- [PyO3 documentation](https://pyo3.rs/v0.15.1/).
|
||||
- [Python documentation](https://docs.python.org/3/).
|
||||
@@ -0,0 +1,14 @@
|
||||
[build-system]
|
||||
requires = ["maturin>=0.12,<0.13"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "rosu-pp-py"
|
||||
requires-python = ">=3.6"
|
||||
classifiers = [
|
||||
"Programming Language :: Rust",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
from typing import Iterable, List, Union
|
||||
|
||||
class ScoreParams:
|
||||
"""
|
||||
A class to describe the state of a score to pass to a `Calculator`.
|
||||
Note that all attributes are optional.
|
||||
|
||||
## Attributes
|
||||
|
||||
`mods`: Optional[int]
|
||||
Bit value for mods, defaults to 0 (NM) see [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
`acc`: Optional[float]
|
||||
Accuracy between 0.0 and 100.0.
|
||||
If neither acc nor hitresults are specified, acc defaults to 100.0.
|
||||
`n300`: Optional[int]
|
||||
Amount of 300s, defaults to value based on acc.
|
||||
`n100`: Optional[int]
|
||||
Amount of 100s, defaults to value based on acc.
|
||||
`n50`: Optional[int]
|
||||
Amount of 50s, defaults to value based on acc.
|
||||
`nMisses`: Optional[int]
|
||||
Amount of misses, defaults to 0.
|
||||
`nKatu`: Optional[int]
|
||||
Amount of katus. Only important for osu!ctb.
|
||||
`combo`: Optional[int]
|
||||
The max combo of the score, defaults to full combo.
|
||||
`score`: Optional[int]
|
||||
The final score. Only important for osu!mania.
|
||||
`passedObjects`: Optional[int]
|
||||
Amount of hitobjects to be considered. Useful for failed plays.
|
||||
Defaults to all objects.
|
||||
|
||||
## Example
|
||||
```py
|
||||
# specify through setter
|
||||
params = ScoreParams()
|
||||
params.acc = 98.76
|
||||
|
||||
# specify through constructor kwargs
|
||||
params = ScoreParams(acc = 98.76)
|
||||
```
|
||||
"""
|
||||
def __init__(self, **kwargs) -> None: ...
|
||||
|
||||
class CalculateResult:
|
||||
"""
|
||||
A class that contains all difficulty and performance attributes.
|
||||
Most attributes are optional based on the map's mode.
|
||||
In the following, O/T/C/M will denote for which mode the given attribute will be present.
|
||||
|
||||
## Attributes
|
||||
|
||||
`mode`: int
|
||||
Game mode of the map. (O/T/C/M)
|
||||
`stars`: float
|
||||
Star rating of the map. (O/T/C/M)
|
||||
`pp`: float
|
||||
Performance points of the score. (O/T/C/M)
|
||||
`ppAcc`: Optional[float]
|
||||
Accuracy based portion of the performance points. (O/T/M)
|
||||
`ppAim`: Optional[float]
|
||||
Aim based portion of the performance points. (O)
|
||||
`ppFlashlight`: Optional[float]
|
||||
Flashlight based portion of the performance points. (O)
|
||||
`ppSpeed`: Optional[float]
|
||||
Speed based portion of the performance points. (O)
|
||||
`ppStrain`: Optional[float]
|
||||
Strain based portion of the performance points. (T/M)
|
||||
`nFruits`: Optional[int]
|
||||
The amount of fruits in the map. (C)
|
||||
`nDroplets`: Optional[int]
|
||||
The amount of droplets in the map. (C)
|
||||
`nTinyDroplets`: Optional[int]
|
||||
The amount of tiny droplets in the map. (C)
|
||||
`aimStrain`: Optional[float]
|
||||
Aim based portion of the star rating. (O)
|
||||
`speedStrain`: Optional[float]
|
||||
Speed based portion of the star rating. (O)
|
||||
`flashlightRating`: Optional[float]
|
||||
Flashlight based portion of the star rating. (O)
|
||||
`sliderFactor`: Optional[float]
|
||||
Nerf factor for sliders. (O)
|
||||
`ar`: float
|
||||
Approach rate of the map. (O/T/C/M)
|
||||
`cs`: float
|
||||
Circle size of the map. (O/T/C/M)
|
||||
`hp`: float
|
||||
Health drain rate of the map. (O/T/C/M)
|
||||
`od`: float
|
||||
Overall difficulty of the map. (O/T/C/M)
|
||||
`bpm`: float
|
||||
Beats per minute of the map. (O/T/C/M)
|
||||
`nCircles`: Optional[int]
|
||||
The amount of circles in the map. (O/T/M)
|
||||
`nSliders`: Optional[int]
|
||||
The amount of sliders in the map. (O/T/M)
|
||||
`nSpinners`: Optional[int]
|
||||
The amount of spinners in the map. (O/T/C)
|
||||
`maxCombo`: Optional[int]
|
||||
The max combo of the map. (O/T/C)
|
||||
"""
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class Calculator:
|
||||
"""
|
||||
A class to calculate difficulty and performance attributes.
|
||||
|
||||
## Arguments
|
||||
|
||||
`path`: str
|
||||
The path to the .osu file.
|
||||
|
||||
## Methods
|
||||
|
||||
`calculate(params)`
|
||||
Calculate the difficulty and performance attributes for the given score parameters.
|
||||
|
||||
## Raises
|
||||
|
||||
Throws an Exception if the map could not be parsed.
|
||||
"""
|
||||
def __init__(self, path: str) -> None: ...
|
||||
|
||||
def calculate(self, params: Union[ScoreParams, Iterable[ScoreParams]]) -> List[CalculateResult]:
|
||||
"""
|
||||
Calculate the difficulty and performance attributes for the given score parameters.
|
||||
|
||||
## Arguments
|
||||
|
||||
`params`: Either a single `ScoreParams` or multiple `ScoreParams` in an iterable collection.
|
||||
|
||||
## Returns
|
||||
|
||||
A list of `CalculateResult` consisting of difficulty and performance attributes for each given `ScoreParams`
|
||||
|
||||
## Example
|
||||
|
||||
```py
|
||||
calculator = Calculator('./maps/1980365.osu')
|
||||
|
||||
params1 = ScoreParams(mods = 8 + 16) # HDHR
|
||||
params2 = ScoreParams(
|
||||
mods = 24,
|
||||
acc = 97.89,
|
||||
nMisses = 13,
|
||||
combo = 1388,
|
||||
)
|
||||
|
||||
# provide params for one score
|
||||
result1 = calculator.calculate(params1)
|
||||
|
||||
# provide params for multiple scores
|
||||
results = calculator.calculate([params1, params2])
|
||||
```
|
||||
"""
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error as StdError,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
};
|
||||
|
||||
use pyo3::{
|
||||
basic::CompareOp,
|
||||
exceptions::{PyException, PyNotImplementedError, PyTypeError},
|
||||
prelude::*,
|
||||
types::{PyDict, PyIterator},
|
||||
PyObjectProtocol,
|
||||
};
|
||||
use rosu_pp::{
|
||||
fruits::FruitsPerformanceAttributes, mania::ManiaPerformanceAttributes,
|
||||
osu::OsuPerformanceAttributes, taiko::TaikoPerformanceAttributes, AnyPP, Beatmap,
|
||||
BeatmapAttributes, BeatmapExt, PerformanceAttributes,
|
||||
};
|
||||
|
||||
#[pyclass]
|
||||
struct Calculator(Beatmap);
|
||||
|
||||
#[pymethods]
|
||||
impl Calculator {
|
||||
#[new]
|
||||
fn new(path: &str) -> PyResult<Self> {
|
||||
Beatmap::from_path(path)
|
||||
.map(Self)
|
||||
.map_err(|e| unwind_error("Failed to parse beatmap", &e))
|
||||
.map_err(PyException::new_err)
|
||||
}
|
||||
|
||||
fn calculate(&self, py: Python, obj: &PyAny) -> PyResult<Vec<CalculateResult>> {
|
||||
match obj.extract::<ScoreParams>() {
|
||||
Ok(params) => {
|
||||
let mods = params.mods;
|
||||
let calculator = params.apply(AnyPP::new(&self.0));
|
||||
let result = CalculateResult::new(calculator.calculate(), &self.0, mods);
|
||||
|
||||
Ok(vec![result])
|
||||
}
|
||||
Err(_) => {
|
||||
let mut mod_diffs = HashMap::new();
|
||||
|
||||
PyIterator::from_object(py, obj)
|
||||
.map_err(|_| {
|
||||
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
|
||||
|
||||
format!(
|
||||
"got '{}'; expected 'ScoreParams' or 'Iterable[ScoreParams]'",
|
||||
py_type
|
||||
)
|
||||
})
|
||||
.map_err(PyTypeError::new_err)?
|
||||
.map(|elem| {
|
||||
let params: ScoreParams = elem?.extract()?;
|
||||
let mods = params.mods;
|
||||
|
||||
let difficulty = mod_diffs
|
||||
.entry((mods, params.passed_objects))
|
||||
.or_insert_with(|| self.0.stars(mods, params.passed_objects))
|
||||
.to_owned();
|
||||
|
||||
let calculator = params.apply(AnyPP::new(&self.0).attributes(difficulty));
|
||||
|
||||
Ok(CalculateResult::new(calculator.calculate(), &self.0, mods))
|
||||
})
|
||||
.collect::<Result<Vec<_>, PyErr>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
struct ScoreParams {
|
||||
#[pyo3(get, set)]
|
||||
mods: u32,
|
||||
#[pyo3(get, set)]
|
||||
n300: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
n100: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
n50: Option<usize>,
|
||||
n_misses: Option<usize>,
|
||||
n_katu: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
acc: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
combo: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
score: Option<u32>,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[allow(non_snake_case)]
|
||||
struct CalculateResult {
|
||||
#[pyo3(get, set)]
|
||||
mode: u8,
|
||||
#[pyo3(get, set)]
|
||||
stars: f64,
|
||||
#[pyo3(get, set)]
|
||||
pp: f64,
|
||||
#[pyo3(get, set)]
|
||||
ppAcc: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppAim: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppFlashlight: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppSpeed: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppStrain: Option<f64>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
nFruits: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nDroplets: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nTinyDroplets: Option<usize>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
aimStrain: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
speedStrain: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
flashlightRating: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
sliderFactor: Option<f64>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
ar: f64,
|
||||
#[pyo3(get, set)]
|
||||
cs: f64,
|
||||
#[pyo3(get, set)]
|
||||
hp: f64,
|
||||
#[pyo3(get, set)]
|
||||
od: f64,
|
||||
#[pyo3(get, set)]
|
||||
bpm: f64,
|
||||
#[pyo3(get, set)]
|
||||
nCircles: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nSliders: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nSpinners: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
maxCombo: Option<usize>,
|
||||
}
|
||||
|
||||
impl CalculateResult {
|
||||
fn new(attrs: PerformanceAttributes, map: &Beatmap, mods: u32) -> Self {
|
||||
let BeatmapAttributes {
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
clock_rate,
|
||||
} = map.attributes().mods(mods);
|
||||
|
||||
let bpm = map.bpm() * clock_rate;
|
||||
|
||||
match attrs {
|
||||
PerformanceAttributes::Fruits(FruitsPerformanceAttributes { pp, difficulty }) => Self {
|
||||
mode: 2,
|
||||
pp,
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.n_fruits + difficulty.n_droplets),
|
||||
nFruits: Some(difficulty.n_fruits),
|
||||
nDroplets: Some(difficulty.n_droplets),
|
||||
nTinyDroplets: Some(difficulty.n_tiny_droplets),
|
||||
nSpinners: Some(map.n_spinners as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Mania(ManiaPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_strain,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 3,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppStrain: Some(pp_strain),
|
||||
stars: difficulty.stars,
|
||||
nCircles: Some(map.n_circles as usize),
|
||||
nSliders: Some(map.n_sliders as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Osu(OsuPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_aim,
|
||||
pp_flashlight,
|
||||
pp_speed,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 0,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppAim: Some(pp_aim),
|
||||
ppFlashlight: Some(pp_flashlight),
|
||||
ppSpeed: Some(pp_speed),
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.max_combo),
|
||||
aimStrain: Some(difficulty.aim_strain),
|
||||
speedStrain: Some(difficulty.speed_strain),
|
||||
flashlightRating: Some(difficulty.flashlight_rating),
|
||||
sliderFactor: Some(difficulty.slider_factor),
|
||||
nCircles: Some(difficulty.n_circles),
|
||||
nSliders: Some(difficulty.n_sliders),
|
||||
nSpinners: Some(difficulty.n_spinners),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_strain,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 1,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppStrain: Some(pp_strain),
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.max_combo),
|
||||
nCircles: Some(map.n_circles as usize),
|
||||
nSliders: Some(map.n_sliders as usize),
|
||||
nSpinners: Some(map.n_spinners as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl CalculateResult {
|
||||
#[new]
|
||||
fn new_() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyproto]
|
||||
impl PyObjectProtocol for CalculateResult {
|
||||
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
|
||||
match (other.extract::<Self>(), op) {
|
||||
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
|
||||
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
|
||||
_ => Err(PyNotImplementedError::new_err("")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> PyResult<String> {
|
||||
Ok(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
|
||||
let mut content = format!("{}: {}\n", cause, e);
|
||||
|
||||
while let Some(src) = e.source() {
|
||||
let _ = writeln!(content, " - caused by: {}", src);
|
||||
e = src;
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
impl ScoreParams {
|
||||
fn apply(self, mut calculator: AnyPP) -> AnyPP {
|
||||
let ScoreParams {
|
||||
mods,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
n_misses,
|
||||
n_katu,
|
||||
acc,
|
||||
combo,
|
||||
score,
|
||||
passed_objects,
|
||||
} = self;
|
||||
|
||||
if let Some(n300) = n300 {
|
||||
calculator = calculator.n300(n300);
|
||||
}
|
||||
|
||||
if let Some(n100) = n100 {
|
||||
calculator = calculator.n100(n100);
|
||||
}
|
||||
|
||||
if let Some(n50) = n50 {
|
||||
calculator = calculator.n50(n50);
|
||||
}
|
||||
|
||||
if let Some(n_misses) = n_misses {
|
||||
calculator = calculator.misses(n_misses);
|
||||
}
|
||||
|
||||
if let Some(n_katu) = n_katu {
|
||||
calculator = calculator.n_katu(n_katu);
|
||||
}
|
||||
|
||||
if let Some(combo) = combo {
|
||||
calculator = calculator.combo(combo);
|
||||
}
|
||||
|
||||
if let Some(passed_objects) = passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(acc) = acc {
|
||||
calculator = calculator.accuracy(acc);
|
||||
}
|
||||
|
||||
if let Some(score) = score {
|
||||
calculator = calculator.score(score);
|
||||
}
|
||||
|
||||
calculator.mods(mods)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ScoreParams {
|
||||
#[new]
|
||||
#[args(kwds = "**")]
|
||||
fn new(kwds: Option<&PyDict>) -> PyResult<Self> {
|
||||
let mut params = Self::default();
|
||||
|
||||
if let Some(dict) = kwds {
|
||||
for (key, value) in dict.iter() {
|
||||
if let Ok(key) = key.extract() {
|
||||
match key {
|
||||
"mods" => params.mods = value.extract()?,
|
||||
"n300" => params.n300 = value.extract()?,
|
||||
"n100" => params.n100 = value.extract()?,
|
||||
"n50" => params.n50 = value.extract()?,
|
||||
"nMisses" => params.n_misses = value.extract()?,
|
||||
"nKatu" => params.n_katu = value.extract()?,
|
||||
"acc" => params.acc = value.extract()?,
|
||||
"combo" => params.combo = value.extract()?,
|
||||
"score" => params.score = value.extract()?,
|
||||
"passedObjects" => params.passed_objects = value.extract()?,
|
||||
_ => {
|
||||
return Err(PyTypeError::new_err(format!(
|
||||
"got an unexpected keyword argument '{}'; expected 'mods', 'n300', \
|
||||
'n100', 'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects'",
|
||||
key,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
#[getter(nMisses)]
|
||||
fn n_misses(&self) -> Option<usize> {
|
||||
self.n_misses
|
||||
}
|
||||
|
||||
#[setter(nMisses)]
|
||||
fn set_n_misses(&mut self, n_misses: usize) {
|
||||
self.n_misses = Some(n_misses);
|
||||
}
|
||||
|
||||
#[getter(nKatu)]
|
||||
fn n_katu(&self) -> Option<usize> {
|
||||
self.n_katu
|
||||
}
|
||||
|
||||
#[setter(nKatu)]
|
||||
fn set_n_katu(&mut self, n_katu: usize) {
|
||||
self.n_katu = Some(n_katu);
|
||||
}
|
||||
|
||||
#[getter(passedObjects)]
|
||||
fn passed_objects(&self) -> Option<usize> {
|
||||
self.passed_objects
|
||||
}
|
||||
|
||||
#[setter(passedObjects)]
|
||||
fn set_passed_objects(&mut self, passed_objects: usize) {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyproto]
|
||||
impl PyObjectProtocol for ScoreParams {
|
||||
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
|
||||
match (other.extract::<Self>(), op) {
|
||||
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
|
||||
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
|
||||
_ => Err(PyNotImplementedError::new_err("")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> PyResult<String> {
|
||||
Ok(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CalculateResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut s = f.debug_struct("CalculateResult");
|
||||
|
||||
s.field("mode", &self.mode)
|
||||
.field("stars", &self.stars)
|
||||
.field("pp", &self.pp);
|
||||
|
||||
if let Some(ref pp_acc) = self.ppAcc {
|
||||
s.field("ppAcc", pp_acc);
|
||||
}
|
||||
|
||||
if let Some(ref pp_aim) = self.ppAim {
|
||||
s.field("ppAim", pp_aim);
|
||||
}
|
||||
|
||||
if let Some(ref pp_flashlight) = self.ppFlashlight {
|
||||
s.field("ppFlashlight", pp_flashlight);
|
||||
}
|
||||
|
||||
if let Some(ref pp_speed) = self.ppSpeed {
|
||||
s.field("ppSpeed", pp_speed);
|
||||
}
|
||||
|
||||
if let Some(ref pp_strain) = self.ppStrain {
|
||||
s.field("ppStrain", pp_strain);
|
||||
}
|
||||
|
||||
if let Some(ref n_fruits) = self.nFruits {
|
||||
s.field("nFruits", n_fruits);
|
||||
}
|
||||
|
||||
if let Some(ref n_droplets) = self.nDroplets {
|
||||
s.field("nDroplets", n_droplets);
|
||||
}
|
||||
|
||||
if let Some(ref n_tiny_droplets) = self.nTinyDroplets {
|
||||
s.field("nTinyDroplets", n_tiny_droplets);
|
||||
}
|
||||
|
||||
if let Some(ref aim_strain) = self.aimStrain {
|
||||
s.field("aimStrain", aim_strain);
|
||||
}
|
||||
|
||||
if let Some(ref speed_strain) = self.speedStrain {
|
||||
s.field("speedStrain", speed_strain);
|
||||
}
|
||||
|
||||
if let Some(ref flashlight_rating) = self.flashlightRating {
|
||||
s.field("flashlightRating", flashlight_rating);
|
||||
}
|
||||
|
||||
if let Some(ref slider_factor) = self.sliderFactor {
|
||||
s.field("sliderFactor", slider_factor);
|
||||
}
|
||||
|
||||
s.field("ar", &self.ar)
|
||||
.field("cs", &self.cs)
|
||||
.field("hp", &self.hp)
|
||||
.field("od", &self.od)
|
||||
.field("bpm", &self.bpm);
|
||||
|
||||
if let Some(ref n_circles) = self.nCircles {
|
||||
s.field("nCircles", n_circles);
|
||||
}
|
||||
|
||||
if let Some(ref n_sliders) = self.nSliders {
|
||||
s.field("nSliders", n_sliders);
|
||||
}
|
||||
|
||||
if let Some(ref n_spinners) = self.nSpinners {
|
||||
s.field("nSpinners", n_spinners);
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ScoreParams {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"ScoreParams {{ \
|
||||
mods: {}, \
|
||||
n300: {}, \
|
||||
n100: {}, \
|
||||
n50: {}, \
|
||||
nMisses: {}, \
|
||||
nKatu: {}, \
|
||||
acc: {}, \
|
||||
combo: {}, \
|
||||
score: {}, \
|
||||
passedObjects: {} \
|
||||
}}",
|
||||
self.mods,
|
||||
match self.n300 {
|
||||
Some(ref n300) => n300 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n100 {
|
||||
Some(ref n100) => n100 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n50 {
|
||||
Some(ref n50) => n50 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n_misses {
|
||||
Some(ref n_misses) => n_misses as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n_katu {
|
||||
Some(ref n_katu) => n_katu as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.acc {
|
||||
Some(ref acc) => acc as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.combo {
|
||||
Some(ref combo) => combo as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.score {
|
||||
Some(ref score) => score as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.passed_objects {
|
||||
Some(ref passed_objects) => passed_objects as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn rosu_pp_py(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<ScoreParams>()?;
|
||||
m.add_class::<Calculator>()?;
|
||||
m.add_class::<CalculateResult>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user