allow customizable map attributes & clock rate

This commit is contained in:
MaxOhn
2022-03-22 00:38:56 +01:00
parent c90e123f6c
commit a1a344e2d5
3 changed files with 184 additions and 14 deletions
+10
View File
@@ -14,6 +14,12 @@ The library exposes three classes: `Calculator`, `ScoreParams`, and `CalculateRe
```py
calculator = Calculator('/path/to/file.osu')
```
Optionally, you can also provide the kwargs `ar`, `cs`, `hp`, or `od` to adjust the map's attributes
or alternatively, after creating the calculator, you can call `set_ar(v)`, `set_cs(v)`, `set_hp(v)`, or `set_od(v)`.
```py
calculator = Calculator('/path/to/file.osu', ar = 10.0)
calculator.set_od(9.2)
```
2) Next, you need to create `ScoreParams`. It has the following fields:
```
mods: Optional[int],
@@ -36,6 +42,8 @@ score: Optional[int],
only relevant for osu!mania
passedObjects: Optional[int],
only consider this many hit objects; useful for failed scores; defaults to all objects
clockRate: Optional[float]
defaults to the mod's clock rate.
```
Note that all fields are optional. If nothing is specified, the parameters are equivalent to the parameters of the best possible NM score.
`ScoreParams` can be created either by calling the constructor without arguments and then set the fields manually like so
@@ -120,6 +128,8 @@ od: float
Overall difficulty of the map. (O/T/C/M)
bpm: float
Beats per minute of the map. (O/T/C/M)
clockRate: float
Clock rate used in calculation i.e. 1.5 for DT, 0.75 for HT, 1.0 for NM or one that was specified (O/T/C/M)
nCircles: Optional[int]
The amount of circles in the map. (O/T/M)
nSliders: Optional[int]
+25 -1
View File
@@ -29,6 +29,9 @@ class ScoreParams:
`passedObjects`: Optional[int]
Amount of hitobjects to be considered. Useful for failed plays.
Defaults to all objects.
`clockRate`: Optional[float]
Customizable clock rate to replace the one dictated by mods.
Defaults to the mod's clock rate.
## Example
```py
@@ -90,6 +93,8 @@ class CalculateResult:
Overall difficulty of the map. (O/T/C/M)
`bpm`: float
Beats per minute of the map. (O/T/C/M)
`clockRate`: float
Clock rate used in calculation i.e. 1.5 for DT, 0.75 for HT, 1.0 for NM or one that was specified (O/T/C/M)
`nCircles`: Optional[int]
The amount of circles in the map. (O/T/M)
`nSliders`: Optional[int]
@@ -110,14 +115,33 @@ class Calculator:
`path`: str
The path to the .osu file.
## Named arguments
`ar`: Optional[float]
Adjusts the map's approach rate.
`cs`: Optional[float]
Adjusts the map's circle size.
`hp`: Optional[float]
Adjusts the map's drain rate.
`od`: Optional[float]
Adjusts the map's overall difficulty.
## Methods
`set_ar(ar)`
Specify an approach rate to override the map's value.
`set_cs(cs)`
Specify a circle size to override the map's value.
`set_hp(hp)`
Specify a drain rate to override the map's value.
`set_od(od)`
Specify an overall difficulty to override the map's value.
`calculate(params)`
Calculate the difficulty and performance attributes for the given score parameters.
## Raises
Throws an Exception if the map could not be parsed.
Throws an Exception if the map could not be parsed or an invalid named argument was given.
"""
def __init__(self, path: str) -> None: ...
+149 -13
View File
@@ -2,6 +2,7 @@ use std::{
collections::HashMap,
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult, Write},
hash::{Hash, Hasher},
};
use pyo3::{
@@ -23,19 +24,79 @@ struct Calculator(Beatmap);
#[pymethods]
impl Calculator {
#[new]
fn new(path: &str) -> PyResult<Self> {
#[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 '{}'; \
expexted 'ar', 'cs', 'hp', 'od'",
key,
)))
}
}
}
}
}
Beatmap::from_path(path)
.map(Self)
.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 calculate(&self, py: Python, obj: &PyAny) -> PyResult<Vec<CalculateResult>> {
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, py: Python, 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);
let result =
CalculateResult::new(calculator.calculate(), &self.0, mods, clock_rate);
Ok(vec![result])
}
@@ -55,9 +116,11 @@ impl Calculator {
.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((mods, params.passed_objects))
.entry(attr_key)
.or_insert_with(|| {
let mut calculator = self.0.stars().mods(mods);
@@ -65,13 +128,19 @@ impl Calculator {
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 calculator = params.apply(AnyPP::new(&self.0).attributes(difficulty));
let attrs = params
.apply(AnyPP::new(&self.0).attributes(difficulty))
.calculate();
Ok(CalculateResult::new(calculator.calculate(), &self.0, mods))
Ok(CalculateResult::new(attrs, &self.0, mods, clock_rate))
})
.collect::<Result<Vec<_>, PyErr>>()
}
@@ -99,6 +168,7 @@ struct ScoreParams {
#[pyo3(get, set)]
score: Option<u32>,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
#[pyclass]
@@ -149,6 +219,8 @@ struct CalculateResult {
#[pyo3(get, set)]
bpm: f64,
#[pyo3(get, set)]
clockRate: f64,
#[pyo3(get, set)]
nCircles: Option<usize>,
#[pyo3(get, set)]
nSliders: Option<usize>,
@@ -159,15 +231,21 @@ struct CalculateResult {
}
impl CalculateResult {
fn new(attrs: PerformanceAttributes, map: &Beatmap, mods: u32) -> Self {
fn new(
attrs: PerformanceAttributes,
map: &Beatmap,
mods: u32,
clock_rate: Option<f64>,
) -> Self {
let BeatmapAttributes {
ar,
cs,
hp,
od,
clock_rate,
clock_rate: clock_rate_,
} = map.attributes().mods(mods);
let clock_rate = clock_rate.unwrap_or(clock_rate_);
let bpm = map.bpm() * clock_rate;
match attrs {
@@ -185,6 +263,7 @@ impl CalculateResult {
hp,
od,
bpm,
clockRate: clock_rate,
..Default::default()
},
PerformanceAttributes::Mania(ManiaPerformanceAttributes {
@@ -205,6 +284,7 @@ impl CalculateResult {
hp,
od,
bpm,
clockRate: clock_rate,
..Default::default()
},
PerformanceAttributes::Osu(OsuPerformanceAttributes {
@@ -235,6 +315,7 @@ impl CalculateResult {
hp,
od,
bpm,
clockRate: clock_rate,
..Default::default()
},
PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
@@ -257,6 +338,7 @@ impl CalculateResult {
hp,
od,
bpm,
clockRate: clock_rate,
..Default::default()
},
}
@@ -298,6 +380,14 @@ fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
}
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 {
mods,
@@ -310,6 +400,7 @@ impl ScoreParams {
combo,
score,
passed_objects,
clock_rate,
} = self;
if let Some(n300) = n300 {
@@ -340,6 +431,10 @@ impl ScoreParams {
calculator = calculator.passed_objects(passed_objects);
}
if let Some(clock_rate) = clock_rate {
calculator = calculator.clock_rate(clock_rate);
}
if let Some(acc) = acc {
calculator = calculator.accuracy(acc);
}
@@ -373,10 +468,11 @@ impl ScoreParams {
"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 'mods', 'n300', \
'n100', 'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects'",
"got an unexpected keyword argument '{}'; expected 'mods', 'n300', 'n100', \
'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects', 'clockRate'",
key,
)))
}
@@ -417,6 +513,16 @@ impl ScoreParams {
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);
}
}
#[pyproto]
@@ -494,7 +600,8 @@ impl Display for CalculateResult {
.field("cs", &self.cs)
.field("hp", &self.hp)
.field("od", &self.od)
.field("bpm", &self.bpm);
.field("bpm", &self.bpm)
.field("clockRate", &self.clockRate);
if let Some(ref n_circles) = self.nCircles {
s.field("nCircles", n_circles);
@@ -526,7 +633,8 @@ impl Display for ScoreParams {
acc: {}, \
combo: {}, \
score: {}, \
passedObjects: {} \
passedObjects: {}, \
clockRate: {} \
}}",
self.mods,
match self.n300 {
@@ -565,6 +673,10 @@ impl Display for ScoreParams {
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,
},
)
}
}
@@ -577,3 +689,27 @@ fn rosu_pp_py(_py: Python, m: &PyModule) -> PyResult<()> {
Ok(())
}
struct AttributeKey {
mods: u32,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
impl Hash for AttributeKey {
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 {
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 {}