added ManiaGradualPerformanceAttributes & minor mania adjustments
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
- [BREAKING] Replaced field `FruitsDifficultyAttributes::max_combo` by a method with the same name
|
- [BREAKING] Replaced field `FruitsDifficultyAttributes::max_combo` by a method with the same name
|
||||||
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
||||||
- Added `ManiaGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
- Added `ManiaGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||||
|
- Added `ManiaGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `ManiaPP` over and over.
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ use super::{DifficultyHitObject, ManiaDifficultyAttributes, STAR_SCALING_FACTOR}
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ManiaGradualDifficultyAttributes<'map> {
|
pub struct ManiaGradualDifficultyAttributes<'map> {
|
||||||
idx: usize,
|
pub(crate) idx: usize,
|
||||||
difficulty_objects: ManiaObjectIter<'map>,
|
difficulty_objects: ManiaObjectIter<'map>,
|
||||||
strain: Strain,
|
strain: Strain,
|
||||||
curr_section_end: f64,
|
curr_section_end: f64,
|
||||||
@@ -79,14 +79,7 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
|||||||
let clock_rate = mods.speed();
|
let clock_rate = mods.speed();
|
||||||
let strain = Strain::new(columns);
|
let strain = Strain::new(columns);
|
||||||
let columns = columns as f32;
|
let columns = columns as f32;
|
||||||
|
let difficulty_objects = ManiaObjectIter::new(&map.hit_objects, columns, clock_rate);
|
||||||
let hit_objects = map.hit_objects.iter().skip(1).zip(map.hit_objects.iter());
|
|
||||||
|
|
||||||
let difficulty_objects = ManiaObjectIter {
|
|
||||||
hit_objects,
|
|
||||||
columns,
|
|
||||||
clock_rate,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
@@ -102,28 +95,25 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
type Item = ManiaDifficultyAttributes;
|
type Item = ManiaDifficultyAttributes;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let h = self.difficulty_objects.next()?;
|
self.idx = self.idx.saturating_add(1);
|
||||||
self.idx += 1;
|
|
||||||
|
|
||||||
let section_len = SECTION_LEN * self.difficulty_objects.clock_rate;
|
|
||||||
|
|
||||||
if self.idx == 1 {
|
if self.idx == 1 {
|
||||||
self.curr_section_end = (h.start_time / section_len).ceil() * section_len;
|
return (!self.difficulty_objects.is_empty).then(ManiaDifficultyAttributes::default);
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = self.difficulty_objects.next()?;
|
||||||
|
|
||||||
|
if self.idx == 2 {
|
||||||
|
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
|
self.strain.process(&h);
|
||||||
|
|
||||||
return Some(ManiaDifficultyAttributes::default());
|
return Some(ManiaDifficultyAttributes::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.idx == 2 {
|
while h.start_time > self.curr_section_end {
|
||||||
while h.base.start_time > self.curr_section_end {
|
|
||||||
self.curr_section_end += section_len;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while h.base.start_time > self.curr_section_end {
|
|
||||||
self.strain.save_current_peak();
|
self.strain.save_current_peak();
|
||||||
let time = self.curr_section_end / self.difficulty_objects.clock_rate;
|
self.strain.start_new_section_from(self.curr_section_end);
|
||||||
self.strain.start_new_section_from(time);
|
self.curr_section_end += SECTION_LEN;
|
||||||
self.curr_section_end += section_len;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.strain.process(&h);
|
self.strain.process(&h);
|
||||||
@@ -145,14 +135,17 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
self.difficulty_objects.size_hint()
|
let (mut len, _) = self.difficulty_objects.size_hint();
|
||||||
|
len += (self.idx == 0) as usize;
|
||||||
|
|
||||||
|
(len, Some(len))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.difficulty_objects.len()
|
self.difficulty_objects.len() + (self.idx == 0) as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +154,21 @@ struct ManiaObjectIter<'map> {
|
|||||||
hit_objects: Zip<Skip<Iter<'map, HitObject>>, Iter<'map, HitObject>>,
|
hit_objects: Zip<Skip<Iter<'map, HitObject>>, Iter<'map, HitObject>>,
|
||||||
columns: f32,
|
columns: f32,
|
||||||
clock_rate: f64,
|
clock_rate: f64,
|
||||||
|
is_empty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> ManiaObjectIter<'map> {
|
||||||
|
fn new(hit_objects: &'map [HitObject], columns: f32, clock_rate: f64) -> Self {
|
||||||
|
let is_empty = hit_objects.is_empty();
|
||||||
|
let hit_objects = hit_objects.iter().skip(1).zip(hit_objects);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
hit_objects,
|
||||||
|
columns,
|
||||||
|
clock_rate,
|
||||||
|
is_empty,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> Iterator for ManiaObjectIter<'map> {
|
impl<'map> Iterator for ManiaObjectIter<'map> {
|
||||||
@@ -173,6 +181,11 @@ impl<'map> Iterator for ManiaObjectIter<'map> {
|
|||||||
|
|
||||||
Some(obj)
|
Some(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.hit_objects.size_hint()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExactSizeIterator for ManiaObjectIter<'_> {
|
impl ExactSizeIterator for ManiaObjectIter<'_> {
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
use crate::{Beatmap, ManiaPP};
|
||||||
|
|
||||||
|
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
|
||||||
|
|
||||||
|
/// Gradually calculate the performance attributes of an osu!mania map.
|
||||||
|
///
|
||||||
|
/// After each hit object you can call
|
||||||
|
/// [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
|
||||||
|
/// and it will return the resulting current [`ManiaPerformanceAttributes`].
|
||||||
|
/// To process multiple objects at once, use
|
||||||
|
/// [`process_next_n_objects`](`ManiaGradualPerformanceAttributes::process_next_n_objects`) instead.
|
||||||
|
///
|
||||||
|
/// Both methods require a play's current score so far.
|
||||||
|
/// Be sure the given score is adjusted with respect to mods.
|
||||||
|
///
|
||||||
|
/// If you only want to calculate difficulty attributes use
|
||||||
|
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes) instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use rosu_pp::{Beatmap, mania::ManiaGradualPerformanceAttributes};
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let map: Beatmap = ...
|
||||||
|
/// # */
|
||||||
|
/// # let map = Beatmap::default();
|
||||||
|
///
|
||||||
|
/// let mods = 64; // DT
|
||||||
|
/// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
/// let mut score = 0;
|
||||||
|
///
|
||||||
|
/// // The first 10 objects each increase the score by 123.
|
||||||
|
/// for _ in 0..10 {
|
||||||
|
/// score += 123;
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let performance = gradual_perf.process_next_object(score).unwrap();
|
||||||
|
/// println!("PP: {}", performance.pp);
|
||||||
|
/// # */
|
||||||
|
/// # let _ = gradual_perf.process_next_object(score);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Then comes a miss so no additional score is added.
|
||||||
|
/// # /*
|
||||||
|
/// let performance = gradual_perf.process_next_object(score).unwrap();
|
||||||
|
/// println!("PP: {}", performance.pp);
|
||||||
|
/// # */
|
||||||
|
/// # let _ = gradual_perf.process_next_object(score);
|
||||||
|
///
|
||||||
|
/// // The next 10 objects give a total of 987 score and will be processed in one go.
|
||||||
|
/// score += 987;
|
||||||
|
/// # /*
|
||||||
|
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap();
|
||||||
|
/// println!("PP: {}", performance.pp);
|
||||||
|
/// # */
|
||||||
|
/// # let _ = gradual_perf.process_next_n_objects(score, 10);
|
||||||
|
///
|
||||||
|
/// // Skip to the end
|
||||||
|
/// # /*
|
||||||
|
/// score = ...
|
||||||
|
/// let final_performance = gradual_perf.process_next_n_objects(score, usize::MAX).unwrap();
|
||||||
|
/// println!("PP: {}", performance.pp);
|
||||||
|
/// # */
|
||||||
|
/// # let _ = gradual_perf.process_next_n_objects(score, usize::MAX);
|
||||||
|
///
|
||||||
|
/// // Once the final performance was calculated,
|
||||||
|
/// // attempting to process further objects will return `None`.
|
||||||
|
/// assert!(gradual_perf.process_next_object(score).is_none());
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ManiaGradualPerformanceAttributes<'map> {
|
||||||
|
difficulty: ManiaGradualDifficultyAttributes<'map>,
|
||||||
|
performance: ManiaPP<'map>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> ManiaGradualPerformanceAttributes<'map> {
|
||||||
|
/// Create a new gradual performance calculator for osu!mania maps.
|
||||||
|
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||||
|
let difficulty = ManiaGradualDifficultyAttributes::new(map, mods);
|
||||||
|
let performance = ManiaPP::new(map).mods(mods).passed_objects(0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
difficulty,
|
||||||
|
performance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process the next hit object and calculate the
|
||||||
|
/// performance attributes for the resulting score.
|
||||||
|
pub fn process_next_object(&mut self, score: u32) -> Option<ManiaPerformanceAttributes> {
|
||||||
|
self.process_next_n_objects(score, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
|
||||||
|
/// but instead of processing only one object it process `n` many.
|
||||||
|
///
|
||||||
|
/// If `n` is 0 it will be considered as 1.
|
||||||
|
/// If there are still objects to be processed but `n` is larger than the amount
|
||||||
|
/// of remaining objects, `n` will be considered as the amount of remaining objects.
|
||||||
|
pub fn process_next_n_objects(
|
||||||
|
&mut self,
|
||||||
|
score: u32,
|
||||||
|
n: usize,
|
||||||
|
) -> Option<ManiaPerformanceAttributes> {
|
||||||
|
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||||
|
let difficulty = self.difficulty.nth(n)?;
|
||||||
|
|
||||||
|
let _ = self.performance.score.insert(score as f64);
|
||||||
|
|
||||||
|
let performance = self
|
||||||
|
.performance
|
||||||
|
.clone()
|
||||||
|
.attributes(difficulty)
|
||||||
|
.passed_objects(self.difficulty.idx)
|
||||||
|
.calculate();
|
||||||
|
|
||||||
|
Some(performance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn correct_empty() {
|
||||||
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
|
||||||
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
assert!(gradual.process_next_n_objects(score, usize::MAX).is_some());
|
||||||
|
assert!(gradual.process_next_object(score).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn next_and_next_n() {
|
||||||
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
|
||||||
|
for _ in 0..20 {
|
||||||
|
let _ = gradual1.process_next_object(score);
|
||||||
|
let _ = gradual2.process_next_object(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = 80;
|
||||||
|
|
||||||
|
for _ in 1..n {
|
||||||
|
let _ = gradual1.process_next_object(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = 100_000;
|
||||||
|
let next = gradual1.process_next_object(score);
|
||||||
|
let next_n = gradual2.process_next_n_objects(score, n);
|
||||||
|
|
||||||
|
assert_eq!(next_n, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn gradual_end_eq_regular() {
|
||||||
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let regular = ManiaPP::new(&map).mods(mods).calculate();
|
||||||
|
|
||||||
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
|
||||||
|
let score = 1_000_000;
|
||||||
|
let gradual_end = gradual.process_next_n_objects(score, usize::MAX).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(regular, gradual_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn gradual_eq_regular_passed() {
|
||||||
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let n = 100;
|
||||||
|
let score = 100_000;
|
||||||
|
|
||||||
|
let regular = ManiaPP::new(&map)
|
||||||
|
.mods(mods)
|
||||||
|
.passed_objects(n)
|
||||||
|
.score(score)
|
||||||
|
.calculate();
|
||||||
|
|
||||||
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
let gradual = gradual.process_next_n_objects(score, n).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(regular, gradual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-17
@@ -69,7 +69,6 @@ fn calculate_strain(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize
|
|||||||
};
|
};
|
||||||
|
|
||||||
let clock_rate = mods.speed();
|
let clock_rate = mods.speed();
|
||||||
let section_len = SECTION_LEN * clock_rate;
|
|
||||||
let mut strain = Strain::new(columns);
|
let mut strain = Strain::new(columns);
|
||||||
let columns = columns as f32;
|
let columns = columns as f32;
|
||||||
|
|
||||||
@@ -81,31 +80,22 @@ fn calculate_strain(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize
|
|||||||
.zip(map.hit_objects.iter())
|
.zip(map.hit_objects.iter())
|
||||||
.map(|(base, prev)| DifficultyHitObject::new(base, prev, columns, clock_rate));
|
.map(|(base, prev)| DifficultyHitObject::new(base, prev, columns, clock_rate));
|
||||||
|
|
||||||
// No strain for first object
|
// Handle first object distinctly
|
||||||
let mut curr_section_end = match map.hit_objects.first() {
|
|
||||||
Some(h) => (h.start_time / section_len).ceil() * section_len,
|
|
||||||
None => return strain,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle second object separately to remove later if-branching
|
|
||||||
let h = match hit_objects.next() {
|
let h = match hit_objects.next() {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => return strain,
|
None => return strain,
|
||||||
};
|
};
|
||||||
|
|
||||||
while h.base.start_time > curr_section_end {
|
// No strain for first object
|
||||||
curr_section_end += section_len;
|
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
}
|
|
||||||
|
|
||||||
strain.process(&h);
|
strain.process(&h);
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for h in hit_objects {
|
for h in hit_objects {
|
||||||
while h.base.start_time > curr_section_end {
|
while h.start_time > curr_section_end {
|
||||||
strain.save_current_peak();
|
strain.save_current_peak();
|
||||||
strain.start_new_section_from(curr_section_end / clock_rate);
|
strain.start_new_section_from(curr_section_end);
|
||||||
|
curr_section_end += SECTION_LEN;
|
||||||
curr_section_end += section_len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strain.process(&h);
|
strain.process(&h);
|
||||||
@@ -147,7 +137,7 @@ pub struct ManiaDifficultyAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a performance calculation on an osu!mania map.
|
/// The result of a performance calculation on an osu!mania map.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
pub struct ManiaPerformanceAttributes {
|
pub struct ManiaPerformanceAttributes {
|
||||||
/// The difficulty attributes that were used for the performance calculation
|
/// The difficulty attributes that were used for the performance calculation
|
||||||
pub difficulty: ManiaDifficultyAttributes,
|
pub difficulty: ManiaDifficultyAttributes,
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,7 @@ pub struct ManiaPP<'map> {
|
|||||||
map: &'map Beatmap,
|
map: &'map Beatmap,
|
||||||
stars: Option<f64>,
|
stars: Option<f64>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
score: Option<f64>,
|
pub(crate) score: Option<f64>,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +83,9 @@ impl<'map> ManiaPP<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||||
|
///
|
||||||
|
/// Be sure you also set [`score`](ManiaPP::score) or the final values
|
||||||
|
/// won't be correct because it will incorrectly assume a score of 1,000,000.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||||
self.passed_objects.replace(passed_objects);
|
self.passed_objects.replace(passed_objects);
|
||||||
|
|||||||
Reference in New Issue
Block a user