initial commit

This commit is contained in:
MaxOhn
2021-12-28 14:24:23 +01:00
commit 74b04ff845
9 changed files with 1314 additions and 0 deletions
+571
View File
@@ -0,0 +1,571 @@
use std::{
collections::HashMap,
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult, Write},
};
use pyo3::{
basic::CompareOp,
exceptions::{PyException, PyNotImplementedError, PyTypeError},
prelude::*,
types::{PyDict, PyIterator},
PyObjectProtocol,
};
use rosu_pp::{
fruits::FruitsPerformanceAttributes, mania::ManiaPerformanceAttributes,
osu::OsuPerformanceAttributes, taiko::TaikoPerformanceAttributes, AnyPP, Beatmap,
BeatmapAttributes, BeatmapExt, PerformanceAttributes,
};
#[pyclass]
struct Calculator(Beatmap);
#[pymethods]
impl Calculator {
#[new]
fn new(path: &str) -> PyResult<Self> {
Beatmap::from_path(path)
.map(Self)
.map_err(|e| unwind_error("Failed to parse beatmap", &e))
.map_err(PyException::new_err)
}
fn calculate(&self, py: Python, obj: &PyAny) -> PyResult<Vec<CalculateResult>> {
match obj.extract::<ScoreParams>() {
Ok(params) => {
let mods = params.mods;
let calculator = params.apply(AnyPP::new(&self.0));
let result = CalculateResult::new(calculator.calculate(), &self.0, mods);
Ok(vec![result])
}
Err(_) => {
let mut mod_diffs = HashMap::new();
PyIterator::from_object(py, obj)
.map_err(|_| {
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
format!(
"got '{}'; expected 'ScoreParams' or 'Iterable[ScoreParams]'",
py_type
)
})
.map_err(PyTypeError::new_err)?
.map(|elem| {
let params: ScoreParams = elem?.extract()?;
let mods = params.mods;
let difficulty = mod_diffs
.entry((mods, params.passed_objects))
.or_insert_with(|| self.0.stars(mods, params.passed_objects))
.to_owned();
let calculator = params.apply(AnyPP::new(&self.0).attributes(difficulty));
Ok(CalculateResult::new(calculator.calculate(), &self.0, mods))
})
.collect::<Result<Vec<_>, PyErr>>()
}
}
}
}
#[pyclass]
#[derive(Clone, Default, PartialEq)]
struct ScoreParams {
#[pyo3(get, set)]
mods: u32,
#[pyo3(get, set)]
n300: Option<usize>,
#[pyo3(get, set)]
n100: Option<usize>,
#[pyo3(get, set)]
n50: Option<usize>,
n_misses: Option<usize>,
n_katu: Option<usize>,
#[pyo3(get, set)]
acc: Option<f64>,
#[pyo3(get, set)]
combo: Option<usize>,
#[pyo3(get, set)]
score: Option<u32>,
passed_objects: Option<usize>,
}
#[pyclass]
#[derive(Clone, Default, PartialEq)]
#[allow(non_snake_case)]
struct CalculateResult {
#[pyo3(get, set)]
mode: u8,
#[pyo3(get, set)]
stars: f64,
#[pyo3(get, set)]
pp: f64,
#[pyo3(get, set)]
ppAcc: Option<f64>,
#[pyo3(get, set)]
ppAim: Option<f64>,
#[pyo3(get, set)]
ppFlashlight: Option<f64>,
#[pyo3(get, set)]
ppSpeed: Option<f64>,
#[pyo3(get, set)]
ppStrain: Option<f64>,
#[pyo3(get, set)]
nFruits: Option<usize>,
#[pyo3(get, set)]
nDroplets: Option<usize>,
#[pyo3(get, set)]
nTinyDroplets: Option<usize>,
#[pyo3(get, set)]
aimStrain: Option<f64>,
#[pyo3(get, set)]
speedStrain: Option<f64>,
#[pyo3(get, set)]
flashlightRating: Option<f64>,
#[pyo3(get, set)]
sliderFactor: Option<f64>,
#[pyo3(get, set)]
ar: f64,
#[pyo3(get, set)]
cs: f64,
#[pyo3(get, set)]
hp: f64,
#[pyo3(get, set)]
od: f64,
#[pyo3(get, set)]
bpm: f64,
#[pyo3(get, set)]
nCircles: Option<usize>,
#[pyo3(get, set)]
nSliders: Option<usize>,
#[pyo3(get, set)]
nSpinners: Option<usize>,
#[pyo3(get, set)]
maxCombo: Option<usize>,
}
impl CalculateResult {
fn new(attrs: PerformanceAttributes, map: &Beatmap, mods: u32) -> Self {
let BeatmapAttributes {
ar,
cs,
hp,
od,
clock_rate,
} = map.attributes().mods(mods);
let bpm = map.bpm() * clock_rate;
match attrs {
PerformanceAttributes::Fruits(FruitsPerformanceAttributes { pp, difficulty }) => Self {
mode: 2,
pp,
stars: difficulty.stars,
maxCombo: Some(difficulty.n_fruits + difficulty.n_droplets),
nFruits: Some(difficulty.n_fruits),
nDroplets: Some(difficulty.n_droplets),
nTinyDroplets: Some(difficulty.n_tiny_droplets),
nSpinners: Some(map.n_spinners as usize),
ar,
cs,
hp,
od,
bpm,
..Default::default()
},
PerformanceAttributes::Mania(ManiaPerformanceAttributes {
pp,
pp_acc,
pp_strain,
difficulty,
}) => Self {
mode: 3,
pp,
ppAcc: Some(pp_acc),
ppStrain: Some(pp_strain),
stars: difficulty.stars,
nCircles: Some(map.n_circles as usize),
nSliders: Some(map.n_sliders as usize),
ar,
cs,
hp,
od,
bpm,
..Default::default()
},
PerformanceAttributes::Osu(OsuPerformanceAttributes {
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
difficulty,
}) => Self {
mode: 0,
pp,
ppAcc: Some(pp_acc),
ppAim: Some(pp_aim),
ppFlashlight: Some(pp_flashlight),
ppSpeed: Some(pp_speed),
stars: difficulty.stars,
maxCombo: Some(difficulty.max_combo),
aimStrain: Some(difficulty.aim_strain),
speedStrain: Some(difficulty.speed_strain),
flashlightRating: Some(difficulty.flashlight_rating),
sliderFactor: Some(difficulty.slider_factor),
nCircles: Some(difficulty.n_circles),
nSliders: Some(difficulty.n_sliders),
nSpinners: Some(difficulty.n_spinners),
ar,
cs,
hp,
od,
bpm,
..Default::default()
},
PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
pp,
pp_acc,
pp_strain,
difficulty,
}) => Self {
mode: 1,
pp,
ppAcc: Some(pp_acc),
ppStrain: Some(pp_strain),
stars: difficulty.stars,
maxCombo: Some(difficulty.max_combo),
nCircles: Some(map.n_circles as usize),
nSliders: Some(map.n_sliders as usize),
nSpinners: Some(map.n_spinners as usize),
ar,
cs,
hp,
od,
bpm,
..Default::default()
},
}
}
}
#[pymethods]
impl CalculateResult {
#[new]
fn new_() -> Self {
Self::default()
}
}
#[pyproto]
impl PyObjectProtocol for CalculateResult {
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
match (other.extract::<Self>(), op) {
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
_ => Err(PyNotImplementedError::new_err("")),
}
}
fn __repr__(&self) -> PyResult<String> {
Ok(self.to_string())
}
}
fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
let mut content = format!("{}: {}\n", cause, e);
while let Some(src) = e.source() {
let _ = writeln!(content, " - caused by: {}", src);
e = src;
}
content
}
impl ScoreParams {
fn apply(self, mut calculator: AnyPP) -> AnyPP {
let ScoreParams {
mods,
n300,
n100,
n50,
n_misses,
n_katu,
acc,
combo,
score,
passed_objects,
} = self;
if let Some(n300) = n300 {
calculator = calculator.n300(n300);
}
if let Some(n100) = n100 {
calculator = calculator.n100(n100);
}
if let Some(n50) = n50 {
calculator = calculator.n50(n50);
}
if let Some(n_misses) = n_misses {
calculator = calculator.misses(n_misses);
}
if let Some(n_katu) = n_katu {
calculator = calculator.n_katu(n_katu);
}
if let Some(combo) = combo {
calculator = calculator.combo(combo);
}
if let Some(passed_objects) = passed_objects {
calculator = calculator.passed_objects(passed_objects);
}
if let Some(acc) = acc {
calculator = calculator.accuracy(acc);
}
if let Some(score) = score {
calculator = calculator.score(score);
}
calculator.mods(mods)
}
}
#[pymethods]
impl ScoreParams {
#[new]
#[args(kwds = "**")]
fn new(kwds: Option<&PyDict>) -> PyResult<Self> {
let mut params = Self::default();
if let Some(dict) = kwds {
for (key, value) in dict.iter() {
if let Ok(key) = key.extract() {
match key {
"mods" => params.mods = value.extract()?,
"n300" => params.n300 = value.extract()?,
"n100" => params.n100 = value.extract()?,
"n50" => params.n50 = value.extract()?,
"nMisses" => params.n_misses = value.extract()?,
"nKatu" => params.n_katu = value.extract()?,
"acc" => params.acc = value.extract()?,
"combo" => params.combo = value.extract()?,
"score" => params.score = value.extract()?,
"passedObjects" => params.passed_objects = value.extract()?,
_ => {
return Err(PyTypeError::new_err(format!(
"got an unexpected keyword argument '{}'; expected 'mods', 'n300', \
'n100', 'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects'",
key,
)))
}
}
}
}
}
Ok(params)
}
#[getter(nMisses)]
fn n_misses(&self) -> Option<usize> {
self.n_misses
}
#[setter(nMisses)]
fn set_n_misses(&mut self, n_misses: usize) {
self.n_misses = Some(n_misses);
}
#[getter(nKatu)]
fn n_katu(&self) -> Option<usize> {
self.n_katu
}
#[setter(nKatu)]
fn set_n_katu(&mut self, n_katu: usize) {
self.n_katu = Some(n_katu);
}
#[getter(passedObjects)]
fn passed_objects(&self) -> Option<usize> {
self.passed_objects
}
#[setter(passedObjects)]
fn set_passed_objects(&mut self, passed_objects: usize) {
self.passed_objects = Some(passed_objects);
}
}
#[pyproto]
impl PyObjectProtocol for ScoreParams {
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
match (other.extract::<Self>(), op) {
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
_ => Err(PyNotImplementedError::new_err("")),
}
}
fn __repr__(&self) -> PyResult<String> {
Ok(self.to_string())
}
}
impl Display for CalculateResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut s = f.debug_struct("CalculateResult");
s.field("mode", &self.mode)
.field("stars", &self.stars)
.field("pp", &self.pp);
if let Some(ref pp_acc) = self.ppAcc {
s.field("ppAcc", pp_acc);
}
if let Some(ref pp_aim) = self.ppAim {
s.field("ppAim", pp_aim);
}
if let Some(ref pp_flashlight) = self.ppFlashlight {
s.field("ppFlashlight", pp_flashlight);
}
if let Some(ref pp_speed) = self.ppSpeed {
s.field("ppSpeed", pp_speed);
}
if let Some(ref pp_strain) = self.ppStrain {
s.field("ppStrain", pp_strain);
}
if let Some(ref n_fruits) = self.nFruits {
s.field("nFruits", n_fruits);
}
if let Some(ref n_droplets) = self.nDroplets {
s.field("nDroplets", n_droplets);
}
if let Some(ref n_tiny_droplets) = self.nTinyDroplets {
s.field("nTinyDroplets", n_tiny_droplets);
}
if let Some(ref aim_strain) = self.aimStrain {
s.field("aimStrain", aim_strain);
}
if let Some(ref speed_strain) = self.speedStrain {
s.field("speedStrain", speed_strain);
}
if let Some(ref flashlight_rating) = self.flashlightRating {
s.field("flashlightRating", flashlight_rating);
}
if let Some(ref slider_factor) = self.sliderFactor {
s.field("sliderFactor", slider_factor);
}
s.field("ar", &self.ar)
.field("cs", &self.cs)
.field("hp", &self.hp)
.field("od", &self.od)
.field("bpm", &self.bpm);
if let Some(ref n_circles) = self.nCircles {
s.field("nCircles", n_circles);
}
if let Some(ref n_sliders) = self.nSliders {
s.field("nSliders", n_sliders);
}
if let Some(ref n_spinners) = self.nSpinners {
s.field("nSpinners", n_spinners);
}
s.finish()
}
}
impl Display for ScoreParams {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"ScoreParams {{ \
mods: {}, \
n300: {}, \
n100: {}, \
n50: {}, \
nMisses: {}, \
nKatu: {}, \
acc: {}, \
combo: {}, \
score: {}, \
passedObjects: {} \
}}",
self.mods,
match self.n300 {
Some(ref n300) => n300 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n100 {
Some(ref n100) => n100 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n50 {
Some(ref n50) => n50 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n_misses {
Some(ref n_misses) => n_misses as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n_katu {
Some(ref n_katu) => n_katu as &dyn Display,
None => &"None" as &dyn Display,
},
match self.acc {
Some(ref acc) => acc as &dyn Display,
None => &"None" as &dyn Display,
},
match self.combo {
Some(ref combo) => combo as &dyn Display,
None => &"None" as &dyn Display,
},
match self.score {
Some(ref score) => score as &dyn Display,
None => &"None" as &dyn Display,
},
match self.passed_objects {
Some(ref passed_objects) => passed_objects as &dyn Display,
None => &"None" as &dyn Display,
},
)
}
}
#[pymodule]
fn rosu_pp_py(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<ScoreParams>()?;
m.add_class::<Calculator>()?;
m.add_class::<CalculateResult>()?;
Ok(())
}