added ManiaGradualDifficultyAttributes
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
- Fixed incorrect attributes on maps with only 1 or 2 hit objects for all modes
|
- Fixed incorrect attributes on maps with only 1 or 2 hit objects for all modes
|
||||||
- [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.
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
use std::{
|
||||||
|
iter::{self, Skip, Zip},
|
||||||
|
slice::Iter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
mania::{strain::Strain, SECTION_LEN},
|
||||||
|
parse::HitObject,
|
||||||
|
Beatmap, GameMode, Mods,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{DifficultyHitObject, ManiaDifficultyAttributes, STAR_SCALING_FACTOR};
|
||||||
|
|
||||||
|
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
||||||
|
///
|
||||||
|
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
|
||||||
|
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
|
||||||
|
/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
|
||||||
|
///
|
||||||
|
/// If you want to calculate performance attributes, use
|
||||||
|
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes) instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use rosu_pp::{Beatmap, mania::ManiaGradualDifficultyAttributes};
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let map: Beatmap = ...
|
||||||
|
/// # */
|
||||||
|
/// # let map = Beatmap::default();
|
||||||
|
///
|
||||||
|
/// let mods = 64; // DT
|
||||||
|
/// let mut iter = ManiaGradualDifficultyAttributes::new(&map, mods);
|
||||||
|
///
|
||||||
|
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
|
||||||
|
/// let attrs2 = iter.next(); // after the second hit object
|
||||||
|
///
|
||||||
|
/// // Remaining hit objects
|
||||||
|
/// for difficulty in iter {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ManiaGradualDifficultyAttributes<'map> {
|
||||||
|
idx: usize,
|
||||||
|
difficulty_objects: ManiaObjectIter<'map>,
|
||||||
|
strain: Strain,
|
||||||
|
curr_section_end: f64,
|
||||||
|
strain_peak_buf: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||||
|
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||||
|
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
||||||
|
let rounded_cs = map.cs.round();
|
||||||
|
|
||||||
|
let columns = match map.mode {
|
||||||
|
GameMode::MNA => rounded_cs.max(1.0) as u8,
|
||||||
|
GameMode::STD => {
|
||||||
|
let rounded_od = map.od.round();
|
||||||
|
|
||||||
|
let n_objects = map.n_circles + map.n_sliders + map.n_spinners;
|
||||||
|
let slider_or_spinner_ratio = (n_objects - map.n_circles) as f32 / n_objects as f32;
|
||||||
|
|
||||||
|
if slider_or_spinner_ratio < 0.2 {
|
||||||
|
7
|
||||||
|
} else if slider_or_spinner_ratio < 0.3 || rounded_cs >= 5.0 {
|
||||||
|
6 + (rounded_od > 5.0) as u8
|
||||||
|
} else if slider_or_spinner_ratio > 0.6 {
|
||||||
|
4 + (rounded_od > 4.0) as u8
|
||||||
|
} else {
|
||||||
|
(rounded_od as u8 + 1).max(4).min(7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("can not calculate mania difficulty on a {:?} map", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let clock_rate = mods.speed();
|
||||||
|
let strain = Strain::new(columns);
|
||||||
|
let columns = columns as f32;
|
||||||
|
|
||||||
|
let hit_objects = map.hit_objects.iter().skip(1).zip(map.hit_objects.iter());
|
||||||
|
|
||||||
|
let difficulty_objects = ManiaObjectIter {
|
||||||
|
hit_objects,
|
||||||
|
columns,
|
||||||
|
clock_rate,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
idx: 0,
|
||||||
|
difficulty_objects,
|
||||||
|
strain,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peak_buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||||
|
type Item = ManiaDifficultyAttributes;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let h = self.difficulty_objects.next()?;
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
let section_len = SECTION_LEN * self.difficulty_objects.clock_rate;
|
||||||
|
|
||||||
|
if self.idx == 1 {
|
||||||
|
self.curr_section_end = (h.start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
return Some(ManiaDifficultyAttributes::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.idx == 2 {
|
||||||
|
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();
|
||||||
|
let time = self.curr_section_end / self.difficulty_objects.clock_rate;
|
||||||
|
self.strain.start_new_section_from(time);
|
||||||
|
self.curr_section_end += section_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.strain.process(&h);
|
||||||
|
|
||||||
|
let missing = self.strain.strain_peaks.len() + 1 - self.strain_peak_buf.len();
|
||||||
|
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
||||||
|
|
||||||
|
self.strain_peak_buf[..self.strain.strain_peaks.len()]
|
||||||
|
.copy_from_slice(&self.strain.strain_peaks);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.strain.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stars = Strain::difficulty_value(&mut self.strain_peak_buf) * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
|
Some(ManiaDifficultyAttributes { stars })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.difficulty_objects.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.difficulty_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ManiaObjectIter<'map> {
|
||||||
|
hit_objects: Zip<Skip<Iter<'map, HitObject>>, Iter<'map, HitObject>>,
|
||||||
|
columns: f32,
|
||||||
|
clock_rate: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> Iterator for ManiaObjectIter<'map> {
|
||||||
|
type Item = DifficultyHitObject<'map>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let (base, prev) = self.hit_objects.next()?;
|
||||||
|
let obj = DifficultyHitObject::new(base, prev, self.columns, self.clock_rate);
|
||||||
|
|
||||||
|
Some(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for ManiaObjectIter<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.hit_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_map() {
|
||||||
|
let map = Beatmap::default();
|
||||||
|
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
|
||||||
|
assert!(attributes.next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn iter_end_eq_regular() {
|
||||||
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let regular = crate::mania::stars(&map, mods, None);
|
||||||
|
|
||||||
|
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
|
||||||
|
.last()
|
||||||
|
.expect("empty iter");
|
||||||
|
|
||||||
|
assert_eq!(regular, iter_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -1,8 +1,12 @@
|
|||||||
#![cfg(feature = "mania")]
|
#![cfg(feature = "mania")]
|
||||||
|
|
||||||
|
mod gradual_difficulty;
|
||||||
|
mod gradual_performance;
|
||||||
mod pp;
|
mod pp;
|
||||||
mod strain;
|
mod strain;
|
||||||
|
|
||||||
|
pub use gradual_difficulty::*;
|
||||||
|
pub use gradual_performance::*;
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use strain::Strain;
|
use strain::Strain;
|
||||||
|
|
||||||
@@ -22,7 +26,7 @@ pub fn stars(
|
|||||||
let mut strain = calculate_strain(map, mods, passed_objects);
|
let mut strain = calculate_strain(map, mods, passed_objects);
|
||||||
|
|
||||||
ManiaDifficultyAttributes {
|
ManiaDifficultyAttributes {
|
||||||
stars: strain.difficulty_value() * STAR_SCALING_FACTOR,
|
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +140,7 @@ impl<'o> DifficultyHitObject<'o> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!mania map.
|
/// The result of a difficulty calculation on an osu!mania map.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
pub struct ManiaDifficultyAttributes {
|
pub struct ManiaDifficultyAttributes {
|
||||||
/// The final star rating.
|
/// The final star rating.
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
|
|||||||
+9
-9
@@ -2,9 +2,10 @@ use super::DifficultyHitObject;
|
|||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Strain {
|
pub(crate) struct Strain {
|
||||||
current_strain: f64,
|
current_strain: f64,
|
||||||
current_section_peak: f64,
|
pub(crate) curr_section_peak: f64,
|
||||||
|
|
||||||
individual_strain: f64,
|
individual_strain: f64,
|
||||||
overall_strain: f64,
|
overall_strain: f64,
|
||||||
@@ -28,7 +29,7 @@ impl Strain {
|
|||||||
pub(crate) fn new(column_count: u8) -> Self {
|
pub(crate) fn new(column_count: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_strain: 1.0,
|
current_strain: 1.0,
|
||||||
current_section_peak: 1.0,
|
curr_section_peak: 1.0,
|
||||||
|
|
||||||
individual_strain: 0.0,
|
individual_strain: 0.0,
|
||||||
overall_strain: 1.0,
|
overall_strain: 1.0,
|
||||||
@@ -43,12 +44,12 @@ impl Strain {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
self.strain_peaks.push(self.curr_section_peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -66,7 +67,7 @@ impl Strain {
|
|||||||
pub(crate) fn process(&mut self, current: &DifficultyHitObject<'_>) {
|
pub(crate) fn process(&mut self, current: &DifficultyHitObject<'_>) {
|
||||||
self.current_strain *= self.strain_decay(current.delta);
|
self.current_strain *= self.strain_decay(current.delta);
|
||||||
self.current_strain += self.strain_value_of(current) * SKILL_MULTIPLIER;
|
self.current_strain += self.strain_value_of(current) * SKILL_MULTIPLIER;
|
||||||
self.current_section_peak = self.current_strain.max(self.current_section_peak);
|
self.curr_section_peak = self.current_strain.max(self.curr_section_peak);
|
||||||
self.prev_time.replace(current.start_time);
|
self.prev_time.replace(current.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +108,13 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
pub(crate) fn difficulty_value(strain_peaks: &mut [f64]) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
self.strain_peaks
|
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
for &strain in self.strain_peaks.iter() {
|
for &strain in strain_peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= DECAY_WEIGHT;
|
weight *= DECAY_WEIGHT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,9 +315,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn empty_map() {
|
fn empty_map() {
|
||||||
let map = Beatmap::default();
|
let map = Beatmap::default();
|
||||||
assert!(OsuGradualDifficultyAttributes::new(&map, 0)
|
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
|
||||||
.next()
|
assert!(attributes.next().is_none());
|
||||||
.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
|||||||
Reference in New Issue
Block a user