This commit is contained in:
MaxOhn
2024-03-22 15:24:19 +01:00
parent 4fd090d625
commit a8e521ef8c
25 changed files with 2905 additions and 1439 deletions
Generated
+51 -28
View File
@@ -21,10 +21,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "indoc"
version = "1.0.6"
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "indoc"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "libc"
@@ -44,9 +50,9 @@ dependencies = [
[[package]]
name = "memoffset"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
@@ -81,25 +87,32 @@ dependencies = [
]
[[package]]
name = "proc-macro2"
version = "1.0.40"
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "proc-macro2"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.18.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd4149c8c3975099622b4e1962dac27565cf5663b76452c3e2b66e0b6824277"
checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
@@ -108,9 +121,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.18.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cd09fe469834db21ee60e0051030339e5d361293d8cb5ec02facf7fdcf52dbf"
checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7"
dependencies = [
"once_cell",
"target-lexicon",
@@ -118,9 +131,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.18.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c427c9a96b9c5b12156dbc11f76b14f49e9aae8905ca783ea87c249044ef137"
checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa"
dependencies = [
"libc",
"pyo3-build-config",
@@ -128,9 +141,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.18.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b822bbba9d60630a44d2109bc410489bb2f439b33e3a14ddeb8a40b378a7c4"
checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -140,20 +153,22 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.18.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ae898104f7c99db06231160770f3e40dad6eb9021daddc0fedfa3e41dff10a"
checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.20"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -168,14 +183,22 @@ dependencies = [
]
[[package]]
name = "rosu-pp"
version = "0.9.4"
name = "rosu-map"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a423903b205528da09d7db2f74facc3d5db3e67890e4c78e2c52bf77bcf00f"
checksum = "3c55926c8f0fed1db12fbe96f7a6083a2c4186443dd32532ab34e6902467a4f3"
[[package]]
name = "rosu-pp"
version = "1.0.0"
source = "git+https://github.com/MaxOhn/rosu-pp?branch=next#711e548a7e29289a42dc8a5ff58ee3e1299218a6"
dependencies = [
"rosu-map",
]
[[package]]
name = "rosu-pp-py"
version = "0.9.4"
version = "1.0.0"
dependencies = [
"pyo3",
"rosu-pp",
@@ -195,9 +218,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]]
name = "syn"
version = "1.0.98"
version = "2.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
dependencies = [
"proc-macro2",
"quote",
@@ -218,9 +241,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "unindent"
version = "0.1.9"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "windows-sys"
+10 -7
View File
@@ -1,20 +1,23 @@
[package]
name = "rosu-pp-py"
version = "0.9.4"
description = "osu! difficulty and pp calculation for all modes"
version = "1.0.0"
edition = "2021"
authors = ["Max Ohn <ohn.m@hotmail.de>"]
license = "MIT"
edition = "2021"
readme = "README.md"
repository = "https://github.com/MaxOhn/rosu-pp-py"
description = "Difficulty and performance calculation for osu!"
[lib]
name = "rosu_pp_py"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.18", features = ["extension-module", "macros"] }
rosu-pp = { version = "0.9.4" }
pyo3 = { version = "0.20.3", features = ["extension-module", "macros"] }
rosu-pp = { git = "https://github.com/MaxOhn/rosu-pp", branch = "next", features = ["sync"] }
[profile.release]
lto = "fat"
lto = true
panic = "abort"
codegen-units = 1
strip = "debuginfo"
strip = true
+18 -78
View File
@@ -1,101 +1,41 @@
# 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 rosu-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`.
TODO
## Example
```py
from rosu_pp_py import Beatmap, Calculator
map = Beatmap(path = "./maps/100.osu")
calc = Calculator(mods = 8)
# Calculate an SS on HD
max_perf = calc.performance(map)
# The mods are still set to HD
calc.set_acc(99.11)
calc.set_n_misses(1)
calc.set_combo(200)
# 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)
curr_perf = calc.performance(map)
print(f'PP: {curr_perf.pp}/{max_perf.pp} | Stars: {max_perf.difficulty.stars}')
map_attrs = calc.map_attributes(map)
print(f'BPM: {map_attrs.bpm}')
strains = calc.strains(map)
print(f'Maximum aim strain: {max(strains.aim)}')
```
TODO
## 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) and ready to go, you can install the project with pip:
```sh
$ pip install rosu-pp-py
```
or
```
$ pip install git+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/).
- [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/
+3 -4
View File
@@ -1,14 +1,13 @@
[build-system]
requires = ["maturin>=0.14,<0.15"]
requires = ["maturin>=1,<2"]
build-backend = "maturin"
[project]
name = "rosu-pp-py"
version = "0.9.4"
version = "1.0.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",
+963 -283
View File
File diff suppressed because it is too large Load Diff
+246
View File
@@ -0,0 +1,246 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyRef, PyResult};
use rosu_pp::model::beatmap::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows};
use crate::{beatmap::PyBeatmap, error::ArgsError, mode::PyGameMode};
#[pyclass(name = "BeatmapAttributesBuilder")]
#[derive(Default)]
pub struct PyBeatmapAttributesBuilder {
mode: Option<PyGameMode>,
is_convert: bool,
mods: u32,
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<&PyDict>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs.iter() {
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 an int"))?
}
"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().mods(self.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;
}
fn set_mode(&mut self, mode: PyGameMode) {
self.mode = Some(mode);
}
fn set_is_convert(&mut self, is_convert: bool) {
self.is_convert = is_convert;
}
fn set_mods(&mut self, mods: u32) {
self.mods = mods;
}
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn set_ar(&mut self, ar: f32, ar_with_mods: bool) {
self.ar = Some(ar);
self.ar_with_mods = ar_with_mods;
}
fn set_cs(&mut self, cs: f32, cs_with_mods: bool) {
self.cs = Some(cs);
self.cs_with_mods = cs_with_mods;
}
fn set_hp(&mut self, hp: f32, hp_with_mods: bool) {
self.hp = Some(hp);
self.hp_with_mods = hp_with_mods;
}
fn set_od(&mut self, od: f32, od_with_mods: bool) {
self.od = Some(od);
self.od_with_mods = od_with_mods;
}
}
define_class! {
#[pyclass(name = "BeatmapAttributes", frozen)]
#[derive(Clone)]
pub struct PyBeatmapAttributes {
pub ar: f64!,
pub od: f64!,
pub cs: f64!,
pub hp: f64!,
pub clock_rate: f64!,
pub ar_hitwindow: f64!,
pub od_hitwindow: f64!,
}
}
impl From<BeatmapAttributes> for PyBeatmapAttributes {
fn from(attrs: BeatmapAttributes) -> Self {
let BeatmapAttributes {
ar,
od,
cs,
hp,
clock_rate,
hit_windows:
HitWindows {
ar: ar_hitwindow,
od: od_hitwindow,
},
} = attrs;
Self {
ar,
od,
cs,
hp,
clock_rate,
ar_hitwindow,
od_hitwindow,
}
}
}
+287
View File
@@ -0,0 +1,287 @@
use pyo3::{pyclass, PyErr};
use rosu_pp::{
any::DifficultyAttributes, catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
};
use crate::{error::ParseError, mode::PyGameMode};
define_class! {
#[pyclass(name = "DifficultyAttributes", frozen)]
#[derive(Clone, Default)]
pub struct PyDifficultyAttributes {
pub mode: PyGameMode!,
pub stars: f64!,
pub is_convert: bool!,
pub aim: f64?,
pub speed: f64?,
pub flashlight: f64?,
pub slider_factor: f64?,
pub speed_note_count: f64?,
pub od: f64?,
pub hp: f64?,
pub n_circles: u32?,
pub n_sliders: u32?,
pub n_spinners: u32?,
pub stamina: f64?,
pub rhythm: f64?,
pub color: f64?,
pub peak: f64?,
pub n_fruits: u32?,
pub n_droplets: u32?,
pub n_tiny_droplets: u32?,
pub n_objects: u32?,
pub ar: f64?,
pub hit_window: f64?,
pub max_combo: u32!,
}
}
impl From<OsuDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: OsuDifficultyAttributes) -> Self {
let OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
} = attrs;
Self {
mode: PyGameMode::Osu,
stars,
is_convert: false,
aim: Some(aim),
speed: Some(speed),
flashlight: Some(flashlight),
slider_factor: Some(slider_factor),
speed_note_count: Some(speed_note_count),
ar: Some(ar),
od: Some(od),
hp: Some(hp),
n_circles: Some(n_circles),
n_sliders: Some(n_sliders),
n_spinners: Some(n_spinners),
max_combo,
..Self::default()
}
}
}
impl From<TaikoDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: TaikoDifficultyAttributes) -> Self {
let TaikoDifficultyAttributes {
stamina,
rhythm,
color,
peak,
hit_window,
stars,
max_combo,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Taiko,
stars,
is_convert,
stamina: Some(stamina),
rhythm: Some(rhythm),
color: Some(color),
peak: Some(peak),
hit_window: Some(hit_window),
max_combo,
..Self::default()
}
}
}
impl From<CatchDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: CatchDifficultyAttributes) -> Self {
let max_combo = attrs.max_combo();
let CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Catch,
stars,
is_convert,
ar: Some(ar),
n_fruits: Some(n_fruits),
n_droplets: Some(n_droplets),
n_tiny_droplets: Some(n_tiny_droplets),
max_combo,
..Self::default()
}
}
}
impl From<ManiaDifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: ManiaDifficultyAttributes) -> Self {
let ManiaDifficultyAttributes {
stars,
hit_window,
n_objects,
max_combo,
is_convert,
} = attrs;
Self {
mode: PyGameMode::Mania,
stars,
is_convert,
hit_window: Some(hit_window),
n_objects: Some(n_objects),
max_combo,
..Self::default()
}
}
}
impl From<DifficultyAttributes> for PyDifficultyAttributes {
fn from(attrs: DifficultyAttributes) -> Self {
match attrs {
DifficultyAttributes::Osu(attrs) => attrs.into(),
DifficultyAttributes::Taiko(attrs) => attrs.into(),
DifficultyAttributes::Catch(attrs) => attrs.into(),
DifficultyAttributes::Mania(attrs) => attrs.into(),
}
}
}
impl TryFrom<PyDifficultyAttributes> for DifficultyAttributes {
type Error = PyErr;
fn try_from(attrs: PyDifficultyAttributes) -> Result<Self, Self::Error> {
let PyDifficultyAttributes {
mode,
stars,
is_convert,
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stamina,
rhythm,
color,
peak,
n_fruits,
n_droplets,
n_tiny_droplets,
n_objects,
ar,
hit_window,
max_combo,
} = attrs;
match mode {
PyGameMode::Osu => {
if let (
Some(aim),
Some(speed),
Some(flashlight),
Some(slider_factor),
Some(speed_note_count),
Some(ar),
Some(od),
Some(hp),
Some(n_circles),
Some(n_sliders),
Some(n_spinners),
) = (
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
) {
return Ok(Self::Osu(OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
}));
}
}
PyGameMode::Taiko => {
if let (Some(stamina), Some(rhythm), Some(color), Some(peak), Some(hit_window)) =
(stamina, rhythm, color, peak, hit_window)
{
return Ok(Self::Taiko(TaikoDifficultyAttributes {
stamina,
rhythm,
color,
peak,
hit_window,
stars,
max_combo,
is_convert,
}));
}
}
PyGameMode::Catch => {
if let (Some(ar), Some(n_fruits), Some(n_droplets), Some(n_tiny_droplets)) =
(ar, n_fruits, n_droplets, n_tiny_droplets)
{
return Ok(Self::Catch(CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
is_convert,
}));
}
}
PyGameMode::Mania => {
if let (Some(hit_window), Some(n_objects)) = (hit_window, n_objects) {
return Ok(Self::Mania(ManiaDifficultyAttributes {
stars,
hit_window,
n_objects,
max_combo,
is_convert,
}));
}
}
}
Err(ParseError::new_err("invalid difficulty attributes"))
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod beatmap;
pub mod difficulty;
pub mod performance;
+112
View File
@@ -0,0 +1,112 @@
use pyo3::pyclass;
use rosu_pp::{
any::PerformanceAttributes, catch::CatchPerformanceAttributes,
mania::ManiaPerformanceAttributes, osu::OsuPerformanceAttributes,
taiko::TaikoPerformanceAttributes,
};
use crate::score_state::PyScoreState;
use super::difficulty::PyDifficultyAttributes;
define_class! {
#[pyclass(name = "PerformanceAttributes", frozen)]
#[derive(Clone, Default)]
pub struct PyPerformanceAttributes {
pub difficulty: PyDifficultyAttributes!,
pub state: PyScoreState?,
pub pp: f64!,
pub pp_aim: f64?,
pub pp_flashlight: f64?,
pub pp_speed: f64?,
pub pp_acc: 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_acc: 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_acc: 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(),
}
}
}
+158 -80
View File
@@ -1,7 +1,18 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyResult};
use rosu_pp::Beatmap;
use std::{error::Error as StdError, fmt::Write};
use crate::error::{ErrorExt, KwargsError, ParseError};
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, 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 {
@@ -13,32 +24,23 @@ impl PyBeatmap {
#[new]
#[pyo3(signature = (**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",
))
}
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;
let mut mode = None;
for (key, value) in kwargs.iter() {
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 +49,176 @@ 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);
map_res = Some(Beatmap::from_bytes(bytes));
}
"ar" => {
"mode" => {
let value = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'ar': must be a real number"))?;
.map_err(|_| PyTypeError::new_err("kwarg 'mode': must be a GameMode"))?;
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);
mode = Some(value);
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'path', \n\
'content', 'bytes', 'ar', 'cs', 'hp', or 'od'"
"unexpected kwarg '{kwarg}': expected 'path', \
'content', 'bytes', or 'mode'"
);
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;
}
return Err(ParseError::new_err(content));
}
None => {
return Err(ArgsError::new_err(
"kwarg 'path', 'bytes', or 'content' must be specified",
))
}
};
let mut this = Self { inner: map };
if let Some(mode) = mode {
this.convert(mode)?;
}
if let Some(cs) = cs {
map.cs = cs;
Ok(this)
}
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));
}
if let Some(hp) = hp {
map.hp = hp;
}
if let Some(od) = od {
map.od = od;
}
Ok(Self { inner: map })
Ok(())
}
fn set_ar(&mut self, ar: f32) {
self.inner.ar = ar;
#[getter]
fn bpm(&self) -> f64 {
self.inner.bpm()
}
fn set_cs(&mut self, cs: f32) {
self.inner.cs = cs;
#[getter]
fn version(&self) -> i32 {
self.inner.version
}
fn set_hp(&mut self, hp: f32) {
self.inner.hp = hp;
#[getter]
fn is_convert(&self) -> bool {
self.inner.is_convert
}
fn set_od(&mut self, od: f32) {
self.inner.od = od;
#[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()
}
}
-276
View File
@@ -1,276 +0,0 @@
use std::borrow::Cow;
use pyo3::{
exceptions::{PyTypeError, PyValueError},
pyclass, pymethods,
types::PyDict,
PyResult,
};
use rosu_pp::{AnyPP, AnyStars, DifficultyAttributes, GameMode};
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]
#[pyo3(signature = (**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, mode) = match self.mode {
Some(mode) => (map.inner.convert_mode(mode), mode),
None => (Cow::Borrowed(&map.inner), map.inner.mode),
};
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(), mode, map.as_ref()))
}
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(&self, map: &PyBeatmap) -> PyResult<PyPerformanceAttributes> {
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())
}
}
-233
View File
@@ -1,233 +0,0 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_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),
}
+214
View File
@@ -0,0 +1,214 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyResult};
use rosu_pp::Difficulty;
use crate::{
attributes::difficulty::PyDifficultyAttributes,
beatmap::PyBeatmap,
error::ArgsError,
gradual::{difficulty::PyGradualDifficulty, performance::PyGradualPerformance},
strains::PyStrains,
};
#[pyclass(name = "Difficulty")]
#[derive(Default)]
pub struct PyDifficulty {
mods: u32,
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,
passed_objects: Option<u32>,
hardrock_offsets: Option<bool>,
}
#[pymethods]
impl PyDifficulty {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs.iter() {
match key.extract()? {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be an int"))?
}
"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 gradual_difficulty(&self, map: &PyBeatmap) -> PyGradualDifficulty {
PyGradualDifficulty::new(self, map)
}
fn gradual_performance(&self, map: &PyBeatmap) -> PyGradualPerformance {
PyGradualPerformance::new(self, map)
}
fn set_mods(&mut self, mods: u32) {
self.mods = mods;
}
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn set_ar(&mut self, ar: f32, ar_with_mods: bool) {
self.ar = Some(ar);
self.ar_with_mods = ar_with_mods;
}
fn set_cs(&mut self, cs: f32, cs_with_mods: bool) {
self.cs = Some(cs);
self.cs_with_mods = cs_with_mods;
}
fn set_hp(&mut self, hp: f32, hp_with_mods: bool) {
self.hp = Some(hp);
self.hp_with_mods = hp_with_mods;
}
fn set_od(&mut self, od: f32, od_with_mods: bool) {
self.od = Some(od);
self.od_with_mods = od_with_mods;
}
fn set_passed_objects(&mut self, passed_objects: u32) {
self.passed_objects = Some(passed_objects);
}
fn set_hardrock_offsets(&mut self, hardrock_offsets: bool) {
self.hardrock_offsets = Some(hardrock_offsets);
}
}
impl PyDifficulty {
pub fn as_difficulty(&self) -> Difficulty {
let mut difficulty = Difficulty::new().mods(self.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
}
}
+2 -21
View File
@@ -1,24 +1,5 @@
use std::{error::Error, fmt::Write};
use pyo3::{create_exception, exceptions::PyException};
create_exception!(rosu_pp_py, KwargsError, PyException);
create_exception!(rosu_pp_py, ArgsError, PyException);
create_exception!(rosu_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, ConvertError, PyException);
+42
View File
@@ -0,0 +1,42 @@
use pyo3::{pyclass, pymethods, PyRef};
use rosu_pp::GradualDifficulty;
use crate::{
attributes::difficulty::PyDifficultyAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
};
#[pyclass(name = "GradualDifficulty")]
pub struct PyGradualDifficulty {
inner: GradualDifficulty,
}
#[pymethods]
impl PyGradualDifficulty {
#[new]
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
Self {
inner: GradualDifficulty::new(difficulty.as_difficulty(), &map.inner),
}
}
fn next(&mut self) -> Option<PyDifficultyAttributes> {
self.inner.next().map(From::from)
}
fn nth(&mut self, n: usize) -> Option<PyDifficultyAttributes> {
self.inner.nth(n).map(From::from)
}
#[getter]
fn n_remaining(&self) -> usize {
self.inner.len()
}
fn __iter__(this: PyRef<'_, Self>) -> PyRef<'_, Self> {
this
}
fn __next__(&mut self) -> Option<PyDifficultyAttributes> {
self.next()
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod difficulty;
pub mod performance;
+35
View File
@@ -0,0 +1,35 @@
use pyo3::{pyclass, pymethods};
use rosu_pp::GradualPerformance;
use crate::{
attributes::performance::PyPerformanceAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
score_state::PyScoreState,
};
#[pyclass(name = "GradualPerformance")]
pub struct PyGradualPerformance {
inner: GradualPerformance,
}
#[pymethods]
impl PyGradualPerformance {
#[new]
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
Self {
inner: GradualPerformance::new(difficulty.as_difficulty(), &map.inner),
}
}
fn next(&mut self, state: &PyScoreState) -> Option<PyPerformanceAttributes> {
self.inner.next(state.into()).map(From::from)
}
fn nth(&mut self, state: &PyScoreState, n: usize) -> Option<PyPerformanceAttributes> {
self.inner.nth(state.into(), n).map(From::from)
}
#[getter]
fn n_remaining(&self) -> usize {
self.inner.len()
}
}
+34 -14
View File
@@ -1,29 +1,48 @@
#![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)]
use error::ConvertError;
use performance::PyHitResultPriority;
use pyo3::{pymodule, types::PyModule, 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 performance;
mod score_state;
mod strains;
#[pymodule]
fn rosu_pp_py(py: Python<'_>, m: &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>()?;
@@ -31,7 +50,8 @@ fn rosu_pp_py(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<PyStrains>()?;
m.add("ParseError", py.get_type::<ParseError>())?;
m.add("KwargsError", py.get_type::<KwargsError>())?;
m.add("ArgsError", py.get_type::<ArgsError>())?;
m.add("ConvertError", py.get_type::<ConvertError>())?;
Ok(())
}
+61
View File
@@ -0,0 +1,61 @@
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 ? ) => {
if let Some(ref $inner_field) = self.$inner_field {
debug.field(stringify!($inner_field), $inner_field);
}
};
( $inner_field:ident ! ) => {
debug.field(stringify!($inner_field), &self.$inner_field);
};
}
$( debug_field!($field $ty_type); )*
debug.finish()
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Debug::fmt(self, f)
}
}
#[pyo3::pymethods]
impl $name {
fn __repr__(&self) -> String {
self.to_string()
}
}
};
( @EXPAND_TY $ty:ident ! ) => {
$ty
};
( @EXPAND_TY $ty:ident ? ) => {
Option<$ty>
};
}
-106
View File
@@ -1,106 +0,0 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_pp::{beatmap::BeatmapAttributes, Beatmap, GameMode};
#[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, mode: GameMode, 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: mode as u8,
version: map.version,
n_circles: map.n_circles,
n_sliders: map.n_sliders,
n_spinners: map.n_spinners,
}
}
}
impl Display for PyBeatmapAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let Self {
ar,
cs,
hp,
od,
ar_hit_window,
od_hit_window,
clock_rate,
bpm,
mode,
version,
n_circles,
n_sliders,
n_spinners,
} = self;
macro_rules! debug {
( $( $field:ident ,)* ) => {
f.debug_struct("BeatmapAttributes")
$( .field(stringify!($field), $field) )*
.finish()
};
}
debug! {
ar,
cs,
hp,
od,
ar_hit_window,
od_hit_window,
clock_rate,
bpm,
mode,
version,
n_circles,
n_sliders,
n_spinners,
}
}
}
#[pymethods]
impl PyBeatmapAttributes {
fn __repr__(&self) -> String {
self.to_string()
}
}
+42
View File
@@ -0,0 +1,42 @@
use std::fmt::{Debug, Formatter, Result as FmtResult};
use pyo3::pyclass;
use rosu_pp::model::mode::GameMode;
#[pyclass(name = "GameMode")]
#[derive(Copy, Clone, Default)]
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,
}
}
}
-145
View File
@@ -1,145 +0,0 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_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),
}
+389
View File
@@ -0,0 +1,389 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyAny, PyRef, PyResult};
use rosu_pp::{
any::{DifficultyAttributes, HitResultPriority},
Difficulty, Performance,
};
use crate::{
attributes::{difficulty::PyDifficultyAttributes, performance::PyPerformanceAttributes},
beatmap::PyBeatmap,
error::ArgsError,
};
#[pyclass(name = "Performance")]
#[derive(Default)]
pub struct PyPerformance {
mods: u32,
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,
passed_objects: Option<u32>,
hardrock_offsets: Option<bool>,
accuracy: Option<f64>,
combo: Option<u32>,
n_geki: Option<u32>,
n_katu: Option<u32>,
n300: Option<u32>,
n100: Option<u32>,
n50: Option<u32>,
misses: Option<u32>,
hitresult_priority: PyHitResultPriority,
}
#[pymethods]
impl PyPerformance {
#[new]
#[pyo3(signature = (**kwargs))]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let mut this = Self::default();
let Some(kwargs) = kwargs else {
return Ok(this);
};
for (key, value) in kwargs.iter() {
match key.extract()? {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be an int"))?
}
"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" => {
let priority: PyHitResultPriority = value.extract().map_err(|_| {
PyTypeError::new_err(
"kwarg 'hitresult_priority': must be a HitResultPriority",
)
})?;
this.hitresult_priority = priority.into()
}
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: &PyAny) -> PyResult<PyPerformanceAttributes> {
let _map;
let mut perf = if let Ok(attrs) = args.extract::<PyPerformanceAttributes>() {
Performance::from_attributes(DifficultyAttributes::try_from(attrs.difficulty)?)
} else if let Ok(attrs) = args.extract::<PyDifficultyAttributes>() {
Performance::from_attributes(DifficultyAttributes::try_from(attrs)?)
} else if let Ok(map) = args.extract::<PyRef<'_, PyBeatmap>>() {
_map = map;
Performance::from_map(&_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 set_mods(&mut self, mods: u32) {
self.mods = mods;
}
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn set_ar(&mut self, ar: f32, ar_with_mods: bool) {
self.ar = Some(ar);
self.ar_with_mods = ar_with_mods;
}
fn set_cs(&mut self, cs: f32, cs_with_mods: bool) {
self.cs = Some(cs);
self.cs_with_mods = cs_with_mods;
}
fn set_hp(&mut self, hp: f32, hp_with_mods: bool) {
self.hp = Some(hp);
self.hp_with_mods = hp_with_mods;
}
fn set_od(&mut self, od: f32, od_with_mods: bool) {
self.od = Some(od);
self.od_with_mods = od_with_mods;
}
fn set_passed_objects(&mut self, passed_objects: u32) {
self.passed_objects = Some(passed_objects);
}
fn set_hardrock_offsets(&mut self, hardrock_offsets: bool) {
self.hardrock_offsets = Some(hardrock_offsets);
}
fn set_accuracy(&mut self, accuracy: f64) {
self.accuracy = Some(accuracy);
}
fn set_combo(&mut self, combo: u32) {
self.combo = Some(combo);
}
fn set_n_geki(&mut self, n_geki: u32) {
self.n_geki = Some(n_geki);
}
fn set_n_katu(&mut self, n_katu: u32) {
self.n_katu = Some(n_katu);
}
fn set_n300(&mut self, n300: u32) {
self.n300 = Some(n300);
}
fn set_n100(&mut self, n100: u32) {
self.n100 = Some(n100);
}
fn set_n50(&mut self, n50: u32) {
self.n50 = Some(n50);
}
fn set_misses(&mut self, misses: u32) {
self.misses = Some(misses);
}
fn set_hitresult_priority(&mut self, hitresult_priority: PyHitResultPriority) {
self.hitresult_priority = hitresult_priority;
}
}
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().mods(self.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(name = "HitResultPriority")]
#[derive(Copy, Clone, Default)]
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,
}
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{exceptions::PyTypeError, pyclass, types::PyDict, 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<&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 {
f.debug_struct("ScoreState")
.field("max_combo", &self.max_combo)
.field("n_geki", &self.n_geki)
.field("n_katu", &self.n_katu)
.field("n300", &self.n300)
.field("n100", &self.n100)
.field("n50", &self.n50)
.field("misses", &self.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
View File
@@ -1,174 +1,103 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{ffi, pyclass, pymethods, types::PyList, IntoPy, IntoPyPointer, Py, PyObject, Python};
use pyo3::pyclass;
use rosu_pp::{
catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains, Strains,
any::Strains, catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains,
};
#[pyclass(name = "Strains")]
#[derive(Debug)]
pub struct PyStrains {
inner: Strains,
use crate::mode::PyGameMode;
type DoubleList = Vec<f64>;
define_class! {
#[pyclass(name = "Strains")]
#[derive(Default)]
pub struct PyStrains {
pub mode: PyGameMode!,
pub section_len: 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_len: 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_len: 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_len: 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_len: 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(),
}
}
}