allow customizable map attributes & clock rate
This commit is contained in:
@@ -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
@@ -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
@@ -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 {}
|
||||||
|
|||||||
Reference in New Issue
Block a user