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 ```py
calculator = Calculator('/path/to/file.osu') 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: 2) Next, you need to create `ScoreParams`. It has the following fields:
``` ```
mods: Optional[int], mods: Optional[int],
@@ -36,6 +42,8 @@ score: Optional[int],
only relevant for osu!mania only relevant for osu!mania
passedObjects: Optional[int], passedObjects: Optional[int],
only consider this many hit objects; useful for failed scores; defaults to all objects 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. 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 `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) Overall difficulty of the map. (O/T/C/M)
bpm: float bpm: float
Beats per minute of the map. (O/T/C/M) 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] nCircles: Optional[int]
The amount of circles in the map. (O/T/M) The amount of circles in the map. (O/T/M)
nSliders: Optional[int] nSliders: Optional[int]
+25 -1
View File
@@ -29,6 +29,9 @@ class ScoreParams:
`passedObjects`: Optional[int] `passedObjects`: Optional[int]
Amount of hitobjects to be considered. Useful for failed plays. Amount of hitobjects to be considered. Useful for failed plays.
Defaults to all objects. 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 ## Example
```py ```py
@@ -90,6 +93,8 @@ class CalculateResult:
Overall difficulty of the map. (O/T/C/M) Overall difficulty of the map. (O/T/C/M)
`bpm`: float `bpm`: float
Beats per minute of the map. (O/T/C/M) 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] `nCircles`: Optional[int]
The amount of circles in the map. (O/T/M) The amount of circles in the map. (O/T/M)
`nSliders`: Optional[int] `nSliders`: Optional[int]
@@ -110,14 +115,33 @@ class Calculator:
`path`: str `path`: str
The path to the .osu file. 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 ## 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(params)`
Calculate the difficulty and performance attributes for the given score parameters. Calculate the difficulty and performance attributes for the given score parameters.
## Raises ## 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: ... def __init__(self, path: str) -> None: ...
+149 -13
View File
@@ -2,6 +2,7 @@ use std::{
collections::HashMap, collections::HashMap,
error::Error as StdError, error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult, Write}, fmt::{Display, Formatter, Result as FmtResult, Write},
hash::{Hash, Hasher},
}; };
use pyo3::{ use pyo3::{
@@ -23,19 +24,79 @@ struct Calculator(Beatmap);
#[pymethods] #[pymethods]
impl Calculator { impl Calculator {
#[new] #[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) 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(|e| unwind_error("Failed to parse beatmap", &e))
.map_err(PyException::new_err) .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>() { match obj.extract::<ScoreParams>() {
Ok(params) => { Ok(params) => {
let mods = params.mods; let mods = params.mods;
let clock_rate = params.clock_rate;
let calculator = params.apply(AnyPP::new(&self.0)); 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]) Ok(vec![result])
} }
@@ -55,9 +116,11 @@ impl Calculator {
.map(|elem| { .map(|elem| {
let params: ScoreParams = elem?.extract()?; let params: ScoreParams = elem?.extract()?;
let mods = params.mods; let mods = params.mods;
let clock_rate = params.clock_rate;
let attr_key = params.as_attr_key();
let difficulty = mod_diffs let difficulty = mod_diffs
.entry((mods, params.passed_objects)) .entry(attr_key)
.or_insert_with(|| { .or_insert_with(|| {
let mut calculator = self.0.stars().mods(mods); let mut calculator = self.0.stars().mods(mods);
@@ -65,13 +128,19 @@ impl Calculator {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
} }
if let Some(clock_rate) = params.clock_rate {
calculator = calculator.clock_rate(clock_rate);
}
calculator.calculate() calculator.calculate()
}) })
.to_owned(); .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>>() .collect::<Result<Vec<_>, PyErr>>()
} }
@@ -99,6 +168,7 @@ struct ScoreParams {
#[pyo3(get, set)] #[pyo3(get, set)]
score: Option<u32>, score: Option<u32>,
passed_objects: Option<usize>, passed_objects: Option<usize>,
clock_rate: Option<f64>,
} }
#[pyclass] #[pyclass]
@@ -149,6 +219,8 @@ struct CalculateResult {
#[pyo3(get, set)] #[pyo3(get, set)]
bpm: f64, bpm: f64,
#[pyo3(get, set)] #[pyo3(get, set)]
clockRate: f64,
#[pyo3(get, set)]
nCircles: Option<usize>, nCircles: Option<usize>,
#[pyo3(get, set)] #[pyo3(get, set)]
nSliders: Option<usize>, nSliders: Option<usize>,
@@ -159,15 +231,21 @@ struct CalculateResult {
} }
impl 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 { let BeatmapAttributes {
ar, ar,
cs, cs,
hp, hp,
od, od,
clock_rate, clock_rate: clock_rate_,
} = map.attributes().mods(mods); } = map.attributes().mods(mods);
let clock_rate = clock_rate.unwrap_or(clock_rate_);
let bpm = map.bpm() * clock_rate; let bpm = map.bpm() * clock_rate;
match attrs { match attrs {
@@ -185,6 +263,7 @@ impl CalculateResult {
hp, hp,
od, od,
bpm, bpm,
clockRate: clock_rate,
..Default::default() ..Default::default()
}, },
PerformanceAttributes::Mania(ManiaPerformanceAttributes { PerformanceAttributes::Mania(ManiaPerformanceAttributes {
@@ -205,6 +284,7 @@ impl CalculateResult {
hp, hp,
od, od,
bpm, bpm,
clockRate: clock_rate,
..Default::default() ..Default::default()
}, },
PerformanceAttributes::Osu(OsuPerformanceAttributes { PerformanceAttributes::Osu(OsuPerformanceAttributes {
@@ -235,6 +315,7 @@ impl CalculateResult {
hp, hp,
od, od,
bpm, bpm,
clockRate: clock_rate,
..Default::default() ..Default::default()
}, },
PerformanceAttributes::Taiko(TaikoPerformanceAttributes { PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
@@ -257,6 +338,7 @@ impl CalculateResult {
hp, hp,
od, od,
bpm, bpm,
clockRate: clock_rate,
..Default::default() ..Default::default()
}, },
} }
@@ -298,6 +380,14 @@ fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
} }
impl ScoreParams { 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 { fn apply(self, mut calculator: AnyPP) -> AnyPP {
let ScoreParams { let ScoreParams {
mods, mods,
@@ -310,6 +400,7 @@ impl ScoreParams {
combo, combo,
score, score,
passed_objects, passed_objects,
clock_rate,
} = self; } = self;
if let Some(n300) = n300 { if let Some(n300) = n300 {
@@ -340,6 +431,10 @@ impl ScoreParams {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
} }
if let Some(clock_rate) = clock_rate {
calculator = calculator.clock_rate(clock_rate);
}
if let Some(acc) = acc { if let Some(acc) = acc {
calculator = calculator.accuracy(acc); calculator = calculator.accuracy(acc);
} }
@@ -373,10 +468,11 @@ impl ScoreParams {
"combo" => params.combo = value.extract()?, "combo" => params.combo = value.extract()?,
"score" => params.score = value.extract()?, "score" => params.score = value.extract()?,
"passedObjects" => params.passed_objects = value.extract()?, "passedObjects" => params.passed_objects = value.extract()?,
"clockRate" => params.clock_rate = value.extract()?,
_ => { _ => {
return Err(PyTypeError::new_err(format!( return Err(PyTypeError::new_err(format!(
"got an unexpected keyword argument '{}'; expected 'mods', 'n300', \ "got an unexpected keyword argument '{}'; expected 'mods', 'n300', 'n100', \
'n100', 'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects'", 'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects', 'clockRate'",
key, key,
))) )))
} }
@@ -417,6 +513,16 @@ impl ScoreParams {
fn set_passed_objects(&mut self, passed_objects: usize) { fn set_passed_objects(&mut self, passed_objects: usize) {
self.passed_objects = Some(passed_objects); 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] #[pyproto]
@@ -494,7 +600,8 @@ impl Display for CalculateResult {
.field("cs", &self.cs) .field("cs", &self.cs)
.field("hp", &self.hp) .field("hp", &self.hp)
.field("od", &self.od) .field("od", &self.od)
.field("bpm", &self.bpm); .field("bpm", &self.bpm)
.field("clockRate", &self.clockRate);
if let Some(ref n_circles) = self.nCircles { if let Some(ref n_circles) = self.nCircles {
s.field("nCircles", n_circles); s.field("nCircles", n_circles);
@@ -526,7 +633,8 @@ impl Display for ScoreParams {
acc: {}, \ acc: {}, \
combo: {}, \ combo: {}, \
score: {}, \ score: {}, \
passedObjects: {} \ passedObjects: {}, \
clockRate: {} \
}}", }}",
self.mods, self.mods,
match self.n300 { match self.n300 {
@@ -565,6 +673,10 @@ impl Display for ScoreParams {
Some(ref passed_objects) => passed_objects as &dyn Display, Some(ref passed_objects) => passed_objects as &dyn Display,
None => &"None" 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(()) 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 {}