initial commit

This commit is contained in:
MaxOhn
2021-12-28 14:24:23 +01:00
commit 74b04ff845
9 changed files with 1314 additions and 0 deletions
+66
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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" }
+21
View File
@@ -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.
+136
View File
@@ -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/).
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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(())
}