rewrite
This commit is contained in:
Generated
+51
-28
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
use pyo3::{pyclass, PyErr};
|
||||
use rosu_pp::{
|
||||
any::DifficultyAttributes, catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
|
||||
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
|
||||
};
|
||||
|
||||
use crate::{error::ParseError, mode::PyGameMode};
|
||||
|
||||
define_class! {
|
||||
#[pyclass(name = "DifficultyAttributes", frozen)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PyDifficultyAttributes {
|
||||
pub mode: PyGameMode!,
|
||||
pub stars: f64!,
|
||||
pub is_convert: bool!,
|
||||
pub aim: f64?,
|
||||
pub speed: f64?,
|
||||
pub flashlight: f64?,
|
||||
pub slider_factor: f64?,
|
||||
pub speed_note_count: f64?,
|
||||
pub od: f64?,
|
||||
pub hp: f64?,
|
||||
pub n_circles: u32?,
|
||||
pub n_sliders: u32?,
|
||||
pub n_spinners: u32?,
|
||||
pub stamina: f64?,
|
||||
pub rhythm: f64?,
|
||||
pub color: f64?,
|
||||
pub peak: f64?,
|
||||
pub n_fruits: u32?,
|
||||
pub n_droplets: u32?,
|
||||
pub n_tiny_droplets: u32?,
|
||||
pub n_objects: u32?,
|
||||
pub ar: f64?,
|
||||
pub hit_window: f64?,
|
||||
pub max_combo: u32!,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsuDifficultyAttributes> for PyDifficultyAttributes {
|
||||
fn from(attrs: OsuDifficultyAttributes) -> Self {
|
||||
let OsuDifficultyAttributes {
|
||||
aim,
|
||||
speed,
|
||||
flashlight,
|
||||
slider_factor,
|
||||
speed_note_count,
|
||||
ar,
|
||||
od,
|
||||
hp,
|
||||
n_circles,
|
||||
n_sliders,
|
||||
n_spinners,
|
||||
stars,
|
||||
max_combo,
|
||||
} = attrs;
|
||||
|
||||
Self {
|
||||
mode: PyGameMode::Osu,
|
||||
stars,
|
||||
is_convert: false,
|
||||
aim: Some(aim),
|
||||
speed: Some(speed),
|
||||
flashlight: Some(flashlight),
|
||||
slider_factor: Some(slider_factor),
|
||||
speed_note_count: Some(speed_note_count),
|
||||
ar: Some(ar),
|
||||
od: Some(od),
|
||||
hp: Some(hp),
|
||||
n_circles: Some(n_circles),
|
||||
n_sliders: Some(n_sliders),
|
||||
n_spinners: Some(n_spinners),
|
||||
max_combo,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TaikoDifficultyAttributes> for PyDifficultyAttributes {
|
||||
fn from(attrs: TaikoDifficultyAttributes) -> Self {
|
||||
let TaikoDifficultyAttributes {
|
||||
stamina,
|
||||
rhythm,
|
||||
color,
|
||||
peak,
|
||||
hit_window,
|
||||
stars,
|
||||
max_combo,
|
||||
is_convert,
|
||||
} = attrs;
|
||||
|
||||
Self {
|
||||
mode: PyGameMode::Taiko,
|
||||
stars,
|
||||
is_convert,
|
||||
stamina: Some(stamina),
|
||||
rhythm: Some(rhythm),
|
||||
color: Some(color),
|
||||
peak: Some(peak),
|
||||
hit_window: Some(hit_window),
|
||||
max_combo,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CatchDifficultyAttributes> for PyDifficultyAttributes {
|
||||
fn from(attrs: CatchDifficultyAttributes) -> Self {
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let CatchDifficultyAttributes {
|
||||
stars,
|
||||
ar,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
is_convert,
|
||||
} = attrs;
|
||||
|
||||
Self {
|
||||
mode: PyGameMode::Catch,
|
||||
stars,
|
||||
is_convert,
|
||||
ar: Some(ar),
|
||||
n_fruits: Some(n_fruits),
|
||||
n_droplets: Some(n_droplets),
|
||||
n_tiny_droplets: Some(n_tiny_droplets),
|
||||
max_combo,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManiaDifficultyAttributes> for PyDifficultyAttributes {
|
||||
fn from(attrs: ManiaDifficultyAttributes) -> Self {
|
||||
let ManiaDifficultyAttributes {
|
||||
stars,
|
||||
hit_window,
|
||||
n_objects,
|
||||
max_combo,
|
||||
is_convert,
|
||||
} = attrs;
|
||||
|
||||
Self {
|
||||
mode: PyGameMode::Mania,
|
||||
stars,
|
||||
is_convert,
|
||||
hit_window: Some(hit_window),
|
||||
n_objects: Some(n_objects),
|
||||
max_combo,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DifficultyAttributes> for PyDifficultyAttributes {
|
||||
fn from(attrs: DifficultyAttributes) -> Self {
|
||||
match attrs {
|
||||
DifficultyAttributes::Osu(attrs) => attrs.into(),
|
||||
DifficultyAttributes::Taiko(attrs) => attrs.into(),
|
||||
DifficultyAttributes::Catch(attrs) => attrs.into(),
|
||||
DifficultyAttributes::Mania(attrs) => attrs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PyDifficultyAttributes> for DifficultyAttributes {
|
||||
type Error = PyErr;
|
||||
|
||||
fn try_from(attrs: PyDifficultyAttributes) -> Result<Self, Self::Error> {
|
||||
let PyDifficultyAttributes {
|
||||
mode,
|
||||
stars,
|
||||
is_convert,
|
||||
aim,
|
||||
speed,
|
||||
flashlight,
|
||||
slider_factor,
|
||||
speed_note_count,
|
||||
od,
|
||||
hp,
|
||||
n_circles,
|
||||
n_sliders,
|
||||
n_spinners,
|
||||
stamina,
|
||||
rhythm,
|
||||
color,
|
||||
peak,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
n_objects,
|
||||
ar,
|
||||
hit_window,
|
||||
max_combo,
|
||||
} = attrs;
|
||||
|
||||
match mode {
|
||||
PyGameMode::Osu => {
|
||||
if let (
|
||||
Some(aim),
|
||||
Some(speed),
|
||||
Some(flashlight),
|
||||
Some(slider_factor),
|
||||
Some(speed_note_count),
|
||||
Some(ar),
|
||||
Some(od),
|
||||
Some(hp),
|
||||
Some(n_circles),
|
||||
Some(n_sliders),
|
||||
Some(n_spinners),
|
||||
) = (
|
||||
aim,
|
||||
speed,
|
||||
flashlight,
|
||||
slider_factor,
|
||||
speed_note_count,
|
||||
ar,
|
||||
od,
|
||||
hp,
|
||||
n_circles,
|
||||
n_sliders,
|
||||
n_spinners,
|
||||
) {
|
||||
return Ok(Self::Osu(OsuDifficultyAttributes {
|
||||
aim,
|
||||
speed,
|
||||
flashlight,
|
||||
slider_factor,
|
||||
speed_note_count,
|
||||
ar,
|
||||
od,
|
||||
hp,
|
||||
n_circles,
|
||||
n_sliders,
|
||||
n_spinners,
|
||||
stars,
|
||||
max_combo,
|
||||
}));
|
||||
}
|
||||
}
|
||||
PyGameMode::Taiko => {
|
||||
if let (Some(stamina), Some(rhythm), Some(color), Some(peak), Some(hit_window)) =
|
||||
(stamina, rhythm, color, peak, hit_window)
|
||||
{
|
||||
return Ok(Self::Taiko(TaikoDifficultyAttributes {
|
||||
stamina,
|
||||
rhythm,
|
||||
color,
|
||||
peak,
|
||||
hit_window,
|
||||
stars,
|
||||
max_combo,
|
||||
is_convert,
|
||||
}));
|
||||
}
|
||||
}
|
||||
PyGameMode::Catch => {
|
||||
if let (Some(ar), Some(n_fruits), Some(n_droplets), Some(n_tiny_droplets)) =
|
||||
(ar, n_fruits, n_droplets, n_tiny_droplets)
|
||||
{
|
||||
return Ok(Self::Catch(CatchDifficultyAttributes {
|
||||
stars,
|
||||
ar,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
is_convert,
|
||||
}));
|
||||
}
|
||||
}
|
||||
PyGameMode::Mania => {
|
||||
if let (Some(hit_window), Some(n_objects)) = (hit_window, n_objects) {
|
||||
return Ok(Self::Mania(ManiaDifficultyAttributes {
|
||||
stars,
|
||||
hit_window,
|
||||
n_objects,
|
||||
max_combo,
|
||||
is_convert,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseError::new_err("invalid difficulty attributes"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod beatmap;
|
||||
pub mod difficulty;
|
||||
pub mod performance;
|
||||
@@ -0,0 +1,112 @@
|
||||
use pyo3::pyclass;
|
||||
use rosu_pp::{
|
||||
any::PerformanceAttributes, catch::CatchPerformanceAttributes,
|
||||
mania::ManiaPerformanceAttributes, osu::OsuPerformanceAttributes,
|
||||
taiko::TaikoPerformanceAttributes,
|
||||
};
|
||||
|
||||
use crate::score_state::PyScoreState;
|
||||
|
||||
use super::difficulty::PyDifficultyAttributes;
|
||||
|
||||
define_class! {
|
||||
#[pyclass(name = "PerformanceAttributes", frozen)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PyPerformanceAttributes {
|
||||
pub difficulty: PyDifficultyAttributes!,
|
||||
pub state: PyScoreState?,
|
||||
pub pp: f64!,
|
||||
pub pp_aim: f64?,
|
||||
pub pp_flashlight: f64?,
|
||||
pub pp_speed: f64?,
|
||||
pub pp_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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use pyo3::{pyclass, pymethods, PyRef};
|
||||
use rosu_pp::GradualDifficulty;
|
||||
|
||||
use crate::{
|
||||
attributes::difficulty::PyDifficultyAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
|
||||
};
|
||||
|
||||
#[pyclass(name = "GradualDifficulty")]
|
||||
pub struct PyGradualDifficulty {
|
||||
inner: GradualDifficulty,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGradualDifficulty {
|
||||
#[new]
|
||||
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
|
||||
Self {
|
||||
inner: GradualDifficulty::new(difficulty.as_difficulty(), &map.inner),
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<PyDifficultyAttributes> {
|
||||
self.inner.next().map(From::from)
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<PyDifficultyAttributes> {
|
||||
self.inner.nth(n).map(From::from)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn n_remaining(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
fn __iter__(this: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
||||
this
|
||||
}
|
||||
|
||||
fn __next__(&mut self) -> Option<PyDifficultyAttributes> {
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod difficulty;
|
||||
pub mod performance;
|
||||
@@ -0,0 +1,35 @@
|
||||
use pyo3::{pyclass, pymethods};
|
||||
use rosu_pp::GradualPerformance;
|
||||
|
||||
use crate::{
|
||||
attributes::performance::PyPerformanceAttributes, beatmap::PyBeatmap, difficulty::PyDifficulty,
|
||||
score_state::PyScoreState,
|
||||
};
|
||||
|
||||
#[pyclass(name = "GradualPerformance")]
|
||||
pub struct PyGradualPerformance {
|
||||
inner: GradualPerformance,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGradualPerformance {
|
||||
#[new]
|
||||
pub fn new(difficulty: &PyDifficulty, map: &PyBeatmap) -> Self {
|
||||
Self {
|
||||
inner: GradualPerformance::new(difficulty.as_difficulty(), &map.inner),
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self, state: &PyScoreState) -> Option<PyPerformanceAttributes> {
|
||||
self.inner.next(state.into()).map(From::from)
|
||||
}
|
||||
|
||||
fn nth(&mut self, state: &PyScoreState, n: usize) -> Option<PyPerformanceAttributes> {
|
||||
self.inner.nth(state.into(), n).map(From::from)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn n_remaining(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
+34
-14
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user