updated rosu-pp & whole rewrite
This commit is contained in:
Generated
+3
-3
@@ -159,13 +159,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rosu-pp"
|
||||
version = "0.8.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "238725936123d4c42c3a4492397d02614798de0881008e7377d78d5db7e0e042"
|
||||
checksum = "70c4f12b757391bb233619b0bb267e424bc7395dcbfa1f85212f16b734784a07"
|
||||
|
||||
[[package]]
|
||||
name = "rosu-pp-py"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
"rosu-pp",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rosu-pp-py"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
description = "osu! difficulty and pp calculation for all modes"
|
||||
authors = ["Max Ohn <ohn.m@hotmail.de>"]
|
||||
license = "MIT"
|
||||
@@ -11,8 +11,8 @@ name = "rosu_pp_py"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
pyo3 = { version = "0.16", features = ["macros", "extension-module"] }
|
||||
rosu-pp = { version = "0.8" }
|
||||
pyo3 = { version = "0.16", features = ["extension-module", "macros"] }
|
||||
rosu-pp = { version = "0.9" }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyResult};
|
||||
use rosu_pp::Beatmap;
|
||||
|
||||
use crate::error::{ErrorExt, KwargsError, ParseError};
|
||||
|
||||
#[pyclass(name = "Beatmap")]
|
||||
pub struct PyBeatmap {
|
||||
pub(crate) inner: Beatmap,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBeatmap {
|
||||
#[new]
|
||||
#[args(kwargs = "**")]
|
||||
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
|
||||
let kwargs = match kwargs {
|
||||
Some(kwargs) => kwargs,
|
||||
None => {
|
||||
return Err(KwargsError::new_err(
|
||||
"'Beatmap' constructor requires kwargs",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(arg) = kwargs.get_item("path") {
|
||||
let path = arg
|
||||
.extract::<&str>()
|
||||
.map_err(|_| PyTypeError::new_err("kwarg 'path': must be a string"))?;
|
||||
|
||||
let map = Beatmap::from_path(path)
|
||||
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
|
||||
|
||||
Self::new_with_attrs(map, kwargs)
|
||||
} else if let Some(arg) = kwargs.get_item("content") {
|
||||
if let Ok(content) = arg.extract::<&str>() {
|
||||
Self::new_from_bytes(content.as_bytes(), kwargs)
|
||||
} else if let Ok(bytes) = arg.extract::<&[u8]>() {
|
||||
Self::new_from_bytes(bytes, kwargs)
|
||||
} else {
|
||||
Err(PyTypeError::new_err(
|
||||
"kwarg 'content': must be a string or a bytearray",
|
||||
))
|
||||
}
|
||||
} else if let Some(arg) = kwargs.get_item("bytes") {
|
||||
let bytes = arg
|
||||
.extract::<&[u8]>()
|
||||
.map_err(|_| PyTypeError::new_err("kwarg 'bytes': must be a bytearray"))?;
|
||||
|
||||
Self::new_from_bytes(bytes, kwargs)
|
||||
} else {
|
||||
Err(KwargsError::new_err(
|
||||
"kwargs must include 'path', 'content', or 'bytes'",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ar(&mut self, ar: f32) {
|
||||
self.inner.ar = ar;
|
||||
}
|
||||
|
||||
fn set_cs(&mut self, cs: f32) {
|
||||
self.inner.cs = cs;
|
||||
}
|
||||
|
||||
fn set_hp(&mut self, hp: f32) {
|
||||
self.inner.hp = hp;
|
||||
}
|
||||
|
||||
fn set_od(&mut self, od: f32) {
|
||||
self.inner.od = od;
|
||||
}
|
||||
}
|
||||
|
||||
impl PyBeatmap {
|
||||
fn new_from_bytes(bytes: &[u8], kwargs: &PyDict) -> PyResult<Self> {
|
||||
let map = Beatmap::from_bytes(bytes)
|
||||
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
|
||||
|
||||
Self::new_with_attrs(map, kwargs)
|
||||
}
|
||||
|
||||
fn new_with_attrs(mut map: Beatmap, kwargs: &PyDict) -> PyResult<Self> {
|
||||
macro_rules! parse_attr {
|
||||
( $( $name:ident ),*) => {
|
||||
$(
|
||||
if let Some(arg) = kwargs.get_item(stringify!($name)) {
|
||||
let value = arg.extract::<f32>().map_err(|_| {
|
||||
PyTypeError::new_err(concat!(
|
||||
"kwarg '",
|
||||
stringify!($name),
|
||||
"': must be a real number"
|
||||
))
|
||||
})?;
|
||||
|
||||
map.$name = value;
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
parse_attr!(ar, cs, hp, od);
|
||||
|
||||
Ok(Self { inner: map })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use pyo3::{exceptions::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]
|
||||
#[derive(Default)]
|
||||
pub struct Calculator {
|
||||
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 Calculator {
|
||||
#[new]
|
||||
#[args(kwargs = "**")]
|
||||
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
|
||||
let kwargs = match kwargs {
|
||||
Some(kwargs) => kwargs,
|
||||
None => return Ok(Self::default()),
|
||||
};
|
||||
|
||||
let mut this = Self::default();
|
||||
|
||||
for (key, value) in kwargs.iter() {
|
||||
match key.extract()? {
|
||||
"mode" => {
|
||||
this.mode = match value.extract::<u8>()? {
|
||||
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()?,
|
||||
"n300" => this.n300 = value.extract()?,
|
||||
"n100" => this.n100 = value.extract()?,
|
||||
"n50" => this.n50 = value.extract()?,
|
||||
"n_misses" => this.n_misses = value.extract()?,
|
||||
"n_geki" => this.n_geki = value.extract()?,
|
||||
"n_katu" => this.n_katu = value.extract()?,
|
||||
"acc" | "accuracy" => this.acc = value.extract()?,
|
||||
"combo" => this.combo = value.extract()?,
|
||||
"passed_objects" => this.passed_objects = value.extract()?,
|
||||
"clock_rate" => this.clock_rate = value.extract()?,
|
||||
"difficulty" | "attributes" => {
|
||||
let attrs = value.extract::<PyDifficultyAttributes>()?;
|
||||
this.attributes = Some(attrs.inner);
|
||||
}
|
||||
kwarg => {
|
||||
let err = format!(
|
||||
"unexpected kwarg '{kwarg}': expected 'mode', 'mods', \n\
|
||||
'n_geki', 'n_katu', 'n300', 'n100', 'n50', 'n_misses', \n\
|
||||
'acc', 'combo', 'passed_objects', 'clock_rate', or 'difficulty'"
|
||||
);
|
||||
|
||||
return Err(KwargsError::new_err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn set_mods(&mut self, mods: u32) {
|
||||
self.mods = Some(mods);
|
||||
}
|
||||
|
||||
fn set_acc(&mut self, acc: f64) {
|
||||
self.acc = Some(acc);
|
||||
}
|
||||
|
||||
fn set_n_geki(&mut self, n_geki: usize) {
|
||||
self.n_geki = Some(n_geki);
|
||||
}
|
||||
|
||||
fn set_n_katu(&mut self, n_katu: usize) {
|
||||
self.n_katu = Some(n_katu);
|
||||
}
|
||||
|
||||
fn set_n300(&mut self, n300: usize) {
|
||||
self.n300 = Some(n300);
|
||||
}
|
||||
|
||||
fn set_n100(&mut self, n100: usize) {
|
||||
self.n100 = Some(n100);
|
||||
}
|
||||
|
||||
fn set_n50(&mut self, n50: usize) {
|
||||
self.n50 = Some(n50);
|
||||
}
|
||||
|
||||
fn set_n_misses(&mut self, n_misses: usize) {
|
||||
self.n_misses = Some(n_misses);
|
||||
}
|
||||
|
||||
fn set_combo(&mut self, combo: usize) {
|
||||
self.combo = Some(combo);
|
||||
}
|
||||
|
||||
fn set_passed_objects(&mut self, passed_objects: usize) {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
}
|
||||
|
||||
fn set_clock_rate(&mut self, clock_rate: f64) {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
}
|
||||
|
||||
fn set_difficulty(&mut self, difficulty: PyDifficultyAttributes) {
|
||||
self.attributes = Some(difficulty.inner);
|
||||
}
|
||||
|
||||
fn map_attributes(&self, map: &PyBeatmap) -> PyResult<PyBeatmapAttributes> {
|
||||
let map = &map.inner;
|
||||
let mut calc = map.attributes();
|
||||
|
||||
if let Some(mode) = self.mode {
|
||||
calc.mode(mode);
|
||||
|
||||
if map.mode != mode && map.mode == GameMode::Osu {
|
||||
calc.converted(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mods) = self.mods {
|
||||
calc.mods(mods);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calc.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
Ok(PyBeatmapAttributes::new(calc.build(), map))
|
||||
}
|
||||
|
||||
fn difficulty(&self, map: &PyBeatmap) -> PyResult<PyDifficultyAttributes> {
|
||||
let mut calc = AnyStars::new(&map.inner);
|
||||
|
||||
set_calc! { calc, self:
|
||||
mode,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
};
|
||||
|
||||
Ok(calc.calculate().into())
|
||||
}
|
||||
|
||||
fn performance(&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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
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,24 @@
|
||||
use std::{error::Error, fmt::Write};
|
||||
|
||||
use pyo3::{create_exception, exceptions::PyException};
|
||||
|
||||
create_exception!(rosu_pp_py, KwargsError, 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
|
||||
}
|
||||
}
|
||||
+15
-897
@@ -1,903 +1,21 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error as StdError,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use pyo3::{
|
||||
basic::CompareOp,
|
||||
exceptions::{PyException, PyNotImplementedError, PyTypeError},
|
||||
prelude::*,
|
||||
types::{PyDict, PyIterator, PyTuple},
|
||||
};
|
||||
use rosu_pp::{
|
||||
beatmap::BeatmapAttributes, catch::CatchPerformanceAttributes,
|
||||
mania::ManiaPerformanceAttributes, osu::OsuPerformanceAttributes,
|
||||
taiko::TaikoPerformanceAttributes, AnyPP, Beatmap, BeatmapExt, GameMode, PerformanceAttributes,
|
||||
Strains as RosuStrains,
|
||||
};
|
||||
|
||||
#[pyclass]
|
||||
struct Calculator(Beatmap);
|
||||
|
||||
#[pymethods]
|
||||
impl Calculator {
|
||||
#[new]
|
||||
#[args(kwds = "**")]
|
||||
fn new(path: &str, kwds: Option<&PyDict>) -> PyResult<Self> {
|
||||
let mut ar = None;
|
||||
let mut cs = None;
|
||||
let mut hp = None;
|
||||
let mut od = None;
|
||||
|
||||
if let Some(dict) = kwds {
|
||||
for (key, value) in dict.iter() {
|
||||
if let Ok(key) = key.extract() {
|
||||
match key {
|
||||
"ar" => ar = Some(value.extract()?),
|
||||
"cs" => cs = Some(value.extract()?),
|
||||
"hp" => hp = Some(value.extract()?),
|
||||
"od" => od = Some(value.extract()?),
|
||||
_ => {
|
||||
return Err(PyTypeError::new_err(format!(
|
||||
"got an unexpected keyword argument '{}'; \
|
||||
expected 'ar', 'cs', 'hp', 'od'",
|
||||
key,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Beatmap::from_path(path)
|
||||
.map(|mut map| {
|
||||
if let Some(ar) = ar {
|
||||
map.ar = ar;
|
||||
}
|
||||
if let Some(cs) = cs {
|
||||
map.cs = cs;
|
||||
}
|
||||
if let Some(hp) = hp {
|
||||
map.hp = hp;
|
||||
}
|
||||
if let Some(od) = od {
|
||||
map.od = od;
|
||||
}
|
||||
|
||||
Self(map)
|
||||
})
|
||||
.map_err(|e| unwind_error("Failed to parse beatmap", &e))
|
||||
.map_err(PyException::new_err)
|
||||
}
|
||||
|
||||
fn set_ar(&mut self, ar: f32) {
|
||||
self.0.ar = ar;
|
||||
}
|
||||
|
||||
fn set_cs(&mut self, cs: f32) {
|
||||
self.0.cs = cs;
|
||||
}
|
||||
|
||||
fn set_hp(&mut self, hp: f32) {
|
||||
self.0.hp = hp;
|
||||
}
|
||||
|
||||
fn set_od(&mut self, od: f32) {
|
||||
self.0.od = od;
|
||||
}
|
||||
|
||||
fn calculate(&mut self, obj: &PyAny) -> PyResult<Vec<CalculateResult>> {
|
||||
match obj.extract::<ScoreParams>() {
|
||||
Ok(params) => {
|
||||
let mods = params.mods;
|
||||
let clock_rate = params.clock_rate;
|
||||
|
||||
let calculator = params.apply(AnyPP::new(&self.0));
|
||||
let result =
|
||||
CalculateResult::new(calculator.calculate(), &self.0, mods, clock_rate);
|
||||
|
||||
Ok(vec![result])
|
||||
}
|
||||
Err(_) => {
|
||||
let mut mod_diffs = HashMap::new();
|
||||
|
||||
PyIterator::from_object(obj.py(), obj)
|
||||
.map_err(|_| {
|
||||
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
|
||||
|
||||
format!(
|
||||
"got '{}'; expected 'ScoreParams' or 'Iterable[ScoreParams]'",
|
||||
py_type
|
||||
)
|
||||
})
|
||||
.map_err(PyTypeError::new_err)?
|
||||
.map(|elem| {
|
||||
let params: ScoreParams = elem?.extract()?;
|
||||
let mods = params.mods;
|
||||
let clock_rate = params.clock_rate;
|
||||
let attr_key = params.as_attr_key();
|
||||
|
||||
let difficulty = mod_diffs
|
||||
.entry(attr_key)
|
||||
.or_insert_with(|| {
|
||||
let mut calculator = self.0.stars().mods(mods);
|
||||
|
||||
if let Some(passed_objects) = params.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = params.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
})
|
||||
.to_owned();
|
||||
|
||||
let attrs = params
|
||||
.apply(AnyPP::new(&self.0).attributes(difficulty))
|
||||
.calculate();
|
||||
|
||||
Ok(CalculateResult::new(attrs, &self.0, mods, clock_rate))
|
||||
})
|
||||
.collect::<Result<Vec<_>, PyErr>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[args(args = "*")]
|
||||
fn strains(&mut self, args: &PyTuple) -> PyResult<Strains> {
|
||||
let mods = if let Ok(obj) = args.get_item(0) {
|
||||
if let Ok(mods) = obj.extract::<u32>() {
|
||||
mods
|
||||
} else {
|
||||
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
|
||||
let err = format!("got '{}'; expected 'int'", py_type);
|
||||
|
||||
return Err(PyTypeError::new_err(err));
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(self.0.strains(mods).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[allow(non_snake_case)]
|
||||
struct Strains {
|
||||
#[pyo3(get, set)]
|
||||
sectionLength: f64,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
color: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
rhythm: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
staminaLeft: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
staminaRight: Option<Vec<f64>>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
aim: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
aimNoSliders: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
speed: Option<Vec<f64>>,
|
||||
#[pyo3(get, set)]
|
||||
flashlight: Option<Vec<f64>>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
strains: Option<Vec<f64>>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
movement: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Strains {
|
||||
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
|
||||
match (other.extract::<Self>(), op) {
|
||||
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
|
||||
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
|
||||
_ => Err(PyNotImplementedError::new_err("")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> PyResult<String> {
|
||||
Ok(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RosuStrains> for Strains {
|
||||
#[inline]
|
||||
fn from(strains: RosuStrains) -> Self {
|
||||
match strains {
|
||||
RosuStrains::Catch(strains) => Self {
|
||||
sectionLength: strains.section_len,
|
||||
movement: Some(strains.movement),
|
||||
..Default::default()
|
||||
},
|
||||
RosuStrains::Mania(strains) => Self {
|
||||
sectionLength: strains.section_len,
|
||||
strains: Some(strains.strains),
|
||||
..Default::default()
|
||||
},
|
||||
RosuStrains::Osu(strains) => Self {
|
||||
sectionLength: strains.section_len,
|
||||
aim: Some(strains.aim),
|
||||
aimNoSliders: Some(strains.aim_no_sliders),
|
||||
speed: Some(strains.speed),
|
||||
flashlight: Some(strains.flashlight),
|
||||
..Default::default()
|
||||
},
|
||||
RosuStrains::Taiko(strains) => Self {
|
||||
sectionLength: strains.section_len,
|
||||
color: Some(strains.color),
|
||||
rhythm: Some(strains.rhythm),
|
||||
staminaLeft: Some(strains.stamina_left),
|
||||
staminaRight: Some(strains.stamina_right),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
struct ScoreParams {
|
||||
#[pyo3(get, set)]
|
||||
mode: Option<u8>,
|
||||
#[pyo3(get, set)]
|
||||
mods: u32,
|
||||
#[pyo3(get, set)]
|
||||
n300: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
n100: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
n50: Option<usize>,
|
||||
n_misses: Option<usize>,
|
||||
n_katu: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
acc: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
combo: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
score: Option<u32>,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[allow(non_snake_case)]
|
||||
struct CalculateResult {
|
||||
#[pyo3(get, set)]
|
||||
mode: u8,
|
||||
#[pyo3(get, set)]
|
||||
stars: f64,
|
||||
#[pyo3(get, set)]
|
||||
pp: f64,
|
||||
#[pyo3(get, set)]
|
||||
ppAcc: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppAim: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppFlashlight: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppSpeed: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
ppStrain: Option<f64>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
nFruits: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nDroplets: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nTinyDroplets: Option<usize>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
aimStrain: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
speedStrain: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
flashlightRating: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
sliderFactor: Option<f64>,
|
||||
|
||||
#[pyo3(get, set)]
|
||||
ar: f64,
|
||||
#[pyo3(get, set)]
|
||||
cs: f64,
|
||||
#[pyo3(get, set)]
|
||||
hp: f64,
|
||||
#[pyo3(get, set)]
|
||||
od: f64,
|
||||
#[pyo3(get, set)]
|
||||
bpm: f64,
|
||||
#[pyo3(get, set)]
|
||||
clockRate: f64,
|
||||
#[pyo3(get, set)]
|
||||
timePreempt: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
greatHitWindow: Option<f64>,
|
||||
#[pyo3(get, set)]
|
||||
nCircles: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nSliders: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
nSpinners: Option<usize>,
|
||||
#[pyo3(get, set)]
|
||||
maxCombo: Option<usize>,
|
||||
}
|
||||
|
||||
impl CalculateResult {
|
||||
fn new(
|
||||
attrs: PerformanceAttributes,
|
||||
map: &Beatmap,
|
||||
mods: u32,
|
||||
clock_rate: Option<f64>,
|
||||
) -> Self {
|
||||
let mut attr_builder = map.attributes();
|
||||
|
||||
if let Some(clock_rate) = clock_rate {
|
||||
attr_builder.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
let mode = match &attrs {
|
||||
PerformanceAttributes::Catch(_) => GameMode::Catch,
|
||||
PerformanceAttributes::Mania(_) => GameMode::Mania,
|
||||
PerformanceAttributes::Osu(_) => GameMode::Osu,
|
||||
PerformanceAttributes::Taiko(_) => GameMode::Taiko,
|
||||
};
|
||||
|
||||
attr_builder.converted(map.mode == GameMode::Osu && mode != GameMode::Osu);
|
||||
|
||||
let BeatmapAttributes {
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
clock_rate,
|
||||
hit_windows,
|
||||
} = attr_builder.mods(mods).mode(mode).build();
|
||||
|
||||
let bpm = map.bpm() * clock_rate;
|
||||
|
||||
match attrs {
|
||||
PerformanceAttributes::Catch(CatchPerformanceAttributes { pp, difficulty }) => Self {
|
||||
mode: 2,
|
||||
pp,
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.n_fruits + difficulty.n_droplets),
|
||||
nFruits: Some(difficulty.n_fruits),
|
||||
nDroplets: Some(difficulty.n_droplets),
|
||||
nTinyDroplets: Some(difficulty.n_tiny_droplets),
|
||||
nSpinners: Some(map.n_spinners as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
clockRate: clock_rate,
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Mania(ManiaPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_strain,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 3,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppStrain: Some(pp_strain),
|
||||
stars: difficulty.stars,
|
||||
nCircles: Some(map.n_circles as usize),
|
||||
nSliders: Some(map.n_sliders as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
clockRate: clock_rate,
|
||||
greatHitWindow: Some(hit_windows.od),
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Osu(OsuPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_aim,
|
||||
pp_flashlight,
|
||||
pp_speed,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 0,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppAim: Some(pp_aim),
|
||||
ppFlashlight: Some(pp_flashlight),
|
||||
ppSpeed: Some(pp_speed),
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.max_combo),
|
||||
aimStrain: Some(difficulty.aim_strain),
|
||||
speedStrain: Some(difficulty.speed_strain),
|
||||
flashlightRating: Some(difficulty.flashlight_rating),
|
||||
sliderFactor: Some(difficulty.slider_factor),
|
||||
nCircles: Some(difficulty.n_circles),
|
||||
nSliders: Some(difficulty.n_sliders),
|
||||
nSpinners: Some(difficulty.n_spinners),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
clockRate: clock_rate,
|
||||
timePreempt: Some(hit_windows.ar),
|
||||
greatHitWindow: Some(hit_windows.od),
|
||||
..Default::default()
|
||||
},
|
||||
PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
|
||||
pp,
|
||||
pp_acc,
|
||||
pp_strain,
|
||||
difficulty,
|
||||
}) => Self {
|
||||
mode: 1,
|
||||
pp,
|
||||
ppAcc: Some(pp_acc),
|
||||
ppStrain: Some(pp_strain),
|
||||
stars: difficulty.stars,
|
||||
maxCombo: Some(difficulty.max_combo),
|
||||
nCircles: Some(map.n_circles as usize),
|
||||
nSliders: Some(map.n_sliders as usize),
|
||||
nSpinners: Some(map.n_spinners as usize),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
od,
|
||||
bpm,
|
||||
clockRate: clock_rate,
|
||||
greatHitWindow: Some(hit_windows.od),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl CalculateResult {
|
||||
#[new]
|
||||
fn new_() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
|
||||
match (other.extract::<Self>(), op) {
|
||||
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
|
||||
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
|
||||
_ => Err(PyNotImplementedError::new_err("")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> PyResult<String> {
|
||||
Ok(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
|
||||
let mut content = format!("{}: {}\n", cause, e);
|
||||
|
||||
while let Some(src) = e.source() {
|
||||
let _ = writeln!(content, " - caused by: {}", src);
|
||||
e = src;
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
impl ScoreParams {
|
||||
fn as_attr_key(&self) -> AttributeKey {
|
||||
AttributeKey {
|
||||
mods: self.mods,
|
||||
passed_objects: self.passed_objects,
|
||||
clock_rate: self.clock_rate,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(self, mut calculator: AnyPP) -> AnyPP {
|
||||
let ScoreParams {
|
||||
mode,
|
||||
mods,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
n_misses,
|
||||
n_katu,
|
||||
acc,
|
||||
combo,
|
||||
score,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
} = self;
|
||||
|
||||
if let Some(mode @ 0..=3) = mode {
|
||||
let mode = match mode {
|
||||
0 => GameMode::Osu,
|
||||
1 => GameMode::Taiko,
|
||||
2 => GameMode::Catch,
|
||||
3 => GameMode::Mania,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
calculator = calculator.mode(mode);
|
||||
}
|
||||
|
||||
if let Some(n300) = n300 {
|
||||
calculator = calculator.n300(n300);
|
||||
}
|
||||
|
||||
if let Some(n100) = n100 {
|
||||
calculator = calculator.n100(n100);
|
||||
}
|
||||
|
||||
if let Some(n50) = n50 {
|
||||
calculator = calculator.n50(n50);
|
||||
}
|
||||
|
||||
if let Some(n_misses) = n_misses {
|
||||
calculator = calculator.misses(n_misses);
|
||||
}
|
||||
|
||||
if let Some(n_katu) = n_katu {
|
||||
calculator = calculator.n_katu(n_katu);
|
||||
}
|
||||
|
||||
if let Some(combo) = combo {
|
||||
calculator = calculator.combo(combo);
|
||||
}
|
||||
|
||||
if let Some(passed_objects) = passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator = calculator.mods(mods);
|
||||
|
||||
if let Some(acc) = acc {
|
||||
calculator = calculator.accuracy(acc);
|
||||
}
|
||||
|
||||
if let Some(score) = score {
|
||||
calculator = calculator.score(score);
|
||||
}
|
||||
|
||||
calculator
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ScoreParams {
|
||||
#[new]
|
||||
#[args(kwds = "**")]
|
||||
fn new(kwds: Option<&PyDict>) -> PyResult<Self> {
|
||||
let mut params = Self::default();
|
||||
|
||||
if let Some(dict) = kwds {
|
||||
for (key, value) in dict.iter() {
|
||||
if let Ok(key) = key.extract() {
|
||||
match key {
|
||||
"mode" => params.mode = Some(value.extract()?),
|
||||
"mods" => params.mods = value.extract()?,
|
||||
"n300" => params.n300 = value.extract()?,
|
||||
"n100" => params.n100 = value.extract()?,
|
||||
"n50" => params.n50 = value.extract()?,
|
||||
"nMisses" => params.n_misses = value.extract()?,
|
||||
"nKatu" => params.n_katu = value.extract()?,
|
||||
"acc" => params.acc = value.extract()?,
|
||||
"combo" => params.combo = value.extract()?,
|
||||
"score" => params.score = value.extract()?,
|
||||
"passedObjects" => params.passed_objects = value.extract()?,
|
||||
"clockRate" => params.clock_rate = value.extract()?,
|
||||
_ => {
|
||||
return Err(PyTypeError::new_err(format!(
|
||||
"got an unexpected keyword argument '{}'; expected 'mode', 'mods', 'n300', 'n100', \
|
||||
'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects', 'clockRate'",
|
||||
key,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
#[getter(nMisses)]
|
||||
fn n_misses(&self) -> Option<usize> {
|
||||
self.n_misses
|
||||
}
|
||||
|
||||
#[setter(nMisses)]
|
||||
fn set_n_misses(&mut self, n_misses: usize) {
|
||||
self.n_misses = Some(n_misses);
|
||||
}
|
||||
|
||||
#[getter(nKatu)]
|
||||
fn n_katu(&self) -> Option<usize> {
|
||||
self.n_katu
|
||||
}
|
||||
|
||||
#[setter(nKatu)]
|
||||
fn set_n_katu(&mut self, n_katu: usize) {
|
||||
self.n_katu = Some(n_katu);
|
||||
}
|
||||
|
||||
#[getter(passedObjects)]
|
||||
fn passed_objects(&self) -> Option<usize> {
|
||||
self.passed_objects
|
||||
}
|
||||
|
||||
#[setter(passedObjects)]
|
||||
fn set_passed_objects(&mut self, passed_objects: usize) {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
}
|
||||
|
||||
#[getter(clockRate)]
|
||||
fn clock_rate(&self) -> Option<f64> {
|
||||
self.clock_rate
|
||||
}
|
||||
|
||||
#[setter(clockRate)]
|
||||
fn set_clock_rate(&mut self, clock_rate: f64) {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
|
||||
match (other.extract::<Self>(), op) {
|
||||
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
|
||||
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
|
||||
_ => Err(PyNotImplementedError::new_err("")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> PyResult<String> {
|
||||
Ok(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Strains {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut s = f.debug_struct("Strains");
|
||||
|
||||
s.field("sectionLength", &self.sectionLength);
|
||||
|
||||
macro_rules! display_field {
|
||||
($self:ident, $s:ident: $($field:ident,)*) => {
|
||||
$(
|
||||
if let Some(ref field) = $self.$field {
|
||||
$s.field(stringify!($field), field);
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
display_field! {
|
||||
self, s:
|
||||
color,
|
||||
rhythm,
|
||||
staminaLeft,
|
||||
staminaRight,
|
||||
aim,
|
||||
aimNoSliders,
|
||||
speed,
|
||||
flashlight,
|
||||
strains,
|
||||
movement,
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CalculateResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut s = f.debug_struct("CalculateResult");
|
||||
|
||||
s.field("mode", &self.mode)
|
||||
.field("stars", &self.stars)
|
||||
.field("pp", &self.pp);
|
||||
|
||||
if let Some(ref pp_acc) = self.ppAcc {
|
||||
s.field("ppAcc", pp_acc);
|
||||
}
|
||||
|
||||
if let Some(ref pp_aim) = self.ppAim {
|
||||
s.field("ppAim", pp_aim);
|
||||
}
|
||||
|
||||
if let Some(ref pp_flashlight) = self.ppFlashlight {
|
||||
s.field("ppFlashlight", pp_flashlight);
|
||||
}
|
||||
|
||||
if let Some(ref pp_speed) = self.ppSpeed {
|
||||
s.field("ppSpeed", pp_speed);
|
||||
}
|
||||
|
||||
if let Some(ref pp_strain) = self.ppStrain {
|
||||
s.field("ppStrain", pp_strain);
|
||||
}
|
||||
|
||||
if let Some(ref n_fruits) = self.nFruits {
|
||||
s.field("nFruits", n_fruits);
|
||||
}
|
||||
|
||||
if let Some(ref n_droplets) = self.nDroplets {
|
||||
s.field("nDroplets", n_droplets);
|
||||
}
|
||||
|
||||
if let Some(ref n_tiny_droplets) = self.nTinyDroplets {
|
||||
s.field("nTinyDroplets", n_tiny_droplets);
|
||||
}
|
||||
|
||||
if let Some(ref aim_strain) = self.aimStrain {
|
||||
s.field("aimStrain", aim_strain);
|
||||
}
|
||||
|
||||
if let Some(ref speed_strain) = self.speedStrain {
|
||||
s.field("speedStrain", speed_strain);
|
||||
}
|
||||
|
||||
if let Some(ref flashlight_rating) = self.flashlightRating {
|
||||
s.field("flashlightRating", flashlight_rating);
|
||||
}
|
||||
|
||||
if let Some(ref slider_factor) = self.sliderFactor {
|
||||
s.field("sliderFactor", slider_factor);
|
||||
}
|
||||
|
||||
s.field("ar", &self.ar)
|
||||
.field("cs", &self.cs)
|
||||
.field("hp", &self.hp)
|
||||
.field("od", &self.od)
|
||||
.field("bpm", &self.bpm)
|
||||
.field("clockRate", &self.clockRate);
|
||||
|
||||
if let Some(ref time_preempt) = self.timePreempt {
|
||||
s.field("timePreempt", time_preempt);
|
||||
}
|
||||
|
||||
if let Some(ref great_hit_window) = self.greatHitWindow {
|
||||
s.field("greatHitWindow", great_hit_window);
|
||||
}
|
||||
|
||||
if let Some(ref n_circles) = self.nCircles {
|
||||
s.field("nCircles", n_circles);
|
||||
}
|
||||
|
||||
if let Some(ref n_sliders) = self.nSliders {
|
||||
s.field("nSliders", n_sliders);
|
||||
}
|
||||
|
||||
if let Some(ref n_spinners) = self.nSpinners {
|
||||
s.field("nSpinners", n_spinners);
|
||||
}
|
||||
|
||||
if let Some(ref combo) = self.maxCombo {
|
||||
s.field("maxCombo", combo);
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ScoreParams {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"ScoreParams {{ \
|
||||
mode: {}, \
|
||||
mods: {}, \
|
||||
n300: {}, \
|
||||
n100: {}, \
|
||||
n50: {}, \
|
||||
nMisses: {}, \
|
||||
nKatu: {}, \
|
||||
acc: {}, \
|
||||
combo: {}, \
|
||||
score: {}, \
|
||||
passedObjects: {}, \
|
||||
clockRate: {} \
|
||||
}}",
|
||||
match self.mode {
|
||||
Some(ref mode) => mode as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
self.mods,
|
||||
match self.n300 {
|
||||
Some(ref n300) => n300 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n100 {
|
||||
Some(ref n100) => n100 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n50 {
|
||||
Some(ref n50) => n50 as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n_misses {
|
||||
Some(ref n_misses) => n_misses as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.n_katu {
|
||||
Some(ref n_katu) => n_katu as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.acc {
|
||||
Some(ref acc) => acc as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.combo {
|
||||
Some(ref combo) => combo as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.score {
|
||||
Some(ref score) => score as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.passed_objects {
|
||||
Some(ref passed_objects) => passed_objects as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
match self.clock_rate {
|
||||
Some(ref clock_rate) => clock_rate as &dyn Display,
|
||||
None => &"None" as &dyn Display,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
#![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)]
|
||||
|
||||
use beatmap::PyBeatmap as Beatmap;
|
||||
use calculator::Calculator;
|
||||
use pyo3::{pymodule, types::PyModule, PyResult, Python};
|
||||
|
||||
mod beatmap;
|
||||
mod calculator;
|
||||
mod diff_attrs;
|
||||
mod error;
|
||||
mod map_attrs;
|
||||
mod perf_attrs;
|
||||
mod strains;
|
||||
|
||||
#[pymodule]
|
||||
fn rosu_pp_py(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<ScoreParams>()?;
|
||||
fn rosu_pp_py(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Beatmap>()?;
|
||||
m.add_class::<Calculator>()?;
|
||||
m.add_class::<CalculateResult>()?;
|
||||
m.add_class::<Strains>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AttributeKey {
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl Hash for AttributeKey {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.mods.hash(state);
|
||||
self.passed_objects.hash(state);
|
||||
(&self.clock_rate as *const _ as *const Option<u64>).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AttributeKey {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.mods == other.mods
|
||||
&& self.passed_objects == other.passed_objects
|
||||
&& self.clock_rate == other.clock_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AttributeKey {}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
|
||||
use pyo3::{pyclass, pymethods};
|
||||
use rosu_pp::{beatmap::BeatmapAttributes, Beatmap};
|
||||
|
||||
#[pyclass(name = "BeatmapAttributes")]
|
||||
#[derive(Debug)]
|
||||
pub struct PyBeatmapAttributes {
|
||||
#[pyo3(get)]
|
||||
ar: f64,
|
||||
#[pyo3(get)]
|
||||
cs: f64,
|
||||
#[pyo3(get)]
|
||||
hp: f64,
|
||||
#[pyo3(get)]
|
||||
od: f64,
|
||||
#[pyo3(get)]
|
||||
ar_hit_window: f64,
|
||||
#[pyo3(get)]
|
||||
od_hit_window: f64,
|
||||
#[pyo3(get)]
|
||||
clock_rate: f64,
|
||||
#[pyo3(get)]
|
||||
bpm: f64,
|
||||
#[pyo3(get)]
|
||||
mode: u8,
|
||||
#[pyo3(get)]
|
||||
version: u8,
|
||||
#[pyo3(get)]
|
||||
n_circles: u32,
|
||||
#[pyo3(get)]
|
||||
n_sliders: u32,
|
||||
#[pyo3(get)]
|
||||
n_spinners: u32,
|
||||
}
|
||||
|
||||
impl PyBeatmapAttributes {
|
||||
pub fn new(attrs: BeatmapAttributes, map: &Beatmap) -> Self {
|
||||
Self {
|
||||
ar: attrs.ar,
|
||||
cs: attrs.cs,
|
||||
hp: attrs.hp,
|
||||
od: attrs.od,
|
||||
ar_hit_window: attrs.hit_windows.ar,
|
||||
od_hit_window: attrs.hit_windows.od,
|
||||
clock_rate: attrs.clock_rate,
|
||||
bpm: map.bpm() * attrs.clock_rate,
|
||||
mode: map.mode as u8,
|
||||
version: map.version,
|
||||
n_circles: map.n_circles,
|
||||
n_sliders: map.n_sliders,
|
||||
n_spinners: map.n_spinners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PyBeatmapAttributes {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
<Self as Debug>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBeatmapAttributes {
|
||||
fn __repr__(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
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),
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
|
||||
use pyo3::{ffi, pyclass, pymethods, types::PyList, IntoPy, IntoPyPointer, Py, PyObject, Python};
|
||||
use rosu_pp::{
|
||||
catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains, Strains,
|
||||
};
|
||||
|
||||
#[pyclass(name = "Strains")]
|
||||
#[derive(Debug)]
|
||||
pub struct PyStrains {
|
||||
inner: Strains,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user