gradual calc for mania
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
use std::fmt::{Debug, Formatter, Result as FmtResult};
|
||||
|
||||
use crate::{
|
||||
any::difficulty::skills::Skill,
|
||||
mania::{object::ObjectParams, ManiaBeatmap},
|
||||
model::{
|
||||
beatmap::HitWindows,
|
||||
hit_object::{HitObject, HitObjectKind},
|
||||
},
|
||||
util::float_ext::FloatExt,
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
use super::{
|
||||
object::ManiaDifficultyObject, skills::strain::Strain, DifficultyValues,
|
||||
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
||||
///
|
||||
/// Note that this struct implements [`Iterator`].
|
||||
/// On every call of [`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
|
||||
/// [`ManiaGradualPerformance`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::mania::ManiaGradualDifficulty;
|
||||
///
|
||||
/// let converted = Beatmap::from_path("./resources/1638954.osu")
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut iter = ManiaGradualDifficulty::new(&difficulty, &converted);
|
||||
///
|
||||
/// // the difficulty of the map after the first hit object
|
||||
/// let attrs1 = iter.next();
|
||||
/// // ... after the second hit object
|
||||
/// let attrs2 = iter.next();
|
||||
///
|
||||
/// // Remaining hit objects
|
||||
/// for difficulty in iter {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`ManiaGradualPerformance`]: crate::mania::ManiaGradualPerformance
|
||||
pub struct ManiaGradualDifficulty<'map> {
|
||||
pub(crate) idx: usize,
|
||||
converted: ManiaBeatmap<'map>,
|
||||
strain: Strain,
|
||||
diff_objects: Box<[ManiaDifficultyObject]>,
|
||||
hit_window: f64,
|
||||
curr_combo: u32,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) clock_rate: f64,
|
||||
}
|
||||
|
||||
impl Debug for ManiaGradualDifficulty<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.debug_struct("ManiaGradualDifficulty")
|
||||
.field("idx", &self.idx)
|
||||
.field("hit_windows", &self.hit_window)
|
||||
.field("curr_combo", &self.curr_combo)
|
||||
.field("mods", &self.mods)
|
||||
.field("clock_rate", &self.clock_rate)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> ManiaGradualDifficulty<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: ManiaBeatmap<'map>) -> Self {
|
||||
let take = difficulty.get_passed_objects();
|
||||
let mods = difficulty.get_mods();
|
||||
let total_columns = converted.map.cs.round_even().max(1.0);
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
let mut params = ObjectParams::new(converted.map.as_ref());
|
||||
|
||||
let HitWindows { od: hit_window, .. } = converted
|
||||
.attributes()
|
||||
.mods(mods)
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let mania_objects = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
||||
.take(take);
|
||||
|
||||
let diff_objects = DifficultyValues::create_difficulty_objects(clock_rate, mania_objects);
|
||||
|
||||
let strain = Strain::new(total_columns as usize);
|
||||
|
||||
let mut curr_combo = 0;
|
||||
|
||||
if let Some(h) = converted.map.hit_objects.first() {
|
||||
let hit_object = ManiaObject::new(h, total_columns, &mut params);
|
||||
|
||||
increment_combo_raw(
|
||||
h,
|
||||
hit_object.start_time,
|
||||
hit_object.end_time,
|
||||
&mut curr_combo,
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
converted,
|
||||
strain,
|
||||
diff_objects,
|
||||
hit_window,
|
||||
curr_combo,
|
||||
mods,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ManiaGradualDifficulty<'_> {
|
||||
type Item = ManiaDifficultyAttributes;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// The first difficulty object belongs to the second note since each difficulty
|
||||
// object requires the current and the last note. Hence, if we're still on the first
|
||||
// object, we don't have a difficulty object yet and just skip processing.
|
||||
if self.idx > 0 {
|
||||
let curr = self.diff_objects.get(self.idx - 1)?;
|
||||
Skill::new(&mut self.strain, &self.diff_objects).process(curr);
|
||||
|
||||
let h = &self.converted.map.hit_objects[self.idx];
|
||||
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
} else if self.converted.map.hit_objects.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.idx += 1;
|
||||
|
||||
Some(ManiaDifficultyAttributes {
|
||||
stars: self.strain.as_difficulty_value() * STAR_SCALING_FACTOR,
|
||||
hit_window: self.hit_window,
|
||||
max_combo: self.curr_combo,
|
||||
n_objects: self.idx as u32,
|
||||
is_convert: self.converted.is_convert,
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.len();
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let skip_iter = self
|
||||
.diff_objects
|
||||
.iter()
|
||||
.zip(self.converted.map.hit_objects.iter().skip(1))
|
||||
.skip(self.idx.saturating_sub(1));
|
||||
|
||||
let mut take = n.min(self.len().saturating_sub(1));
|
||||
|
||||
// The first note has no difficulty object
|
||||
if self.idx == 0 && take > 0 {
|
||||
take -= 1;
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
let mut strain = Skill::new(&mut self.strain, &self.diff_objects);
|
||||
|
||||
for (curr, h) in skip_iter.take(take) {
|
||||
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
strain.process(curr);
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for ManiaGradualDifficulty<'_> {
|
||||
fn len(&self) -> usize {
|
||||
self.diff_objects.len() + 1 - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_combo(
|
||||
h: &HitObject,
|
||||
diff_obj: &ManiaDifficultyObject,
|
||||
curr_combo: &mut u32,
|
||||
clock_rate: f64,
|
||||
) {
|
||||
increment_combo_raw(
|
||||
h,
|
||||
diff_obj.start_time * clock_rate,
|
||||
diff_obj.end_time * clock_rate,
|
||||
curr_combo,
|
||||
);
|
||||
}
|
||||
|
||||
fn increment_combo_raw(h: &HitObject, start_time: f64, end_time: f64, curr_combo: &mut u32) {
|
||||
match h.kind {
|
||||
HitObjectKind::Circle => *curr_combo += 1,
|
||||
_ => *curr_combo += 1 + ((end_time - start_time) / 100.0) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{mania::Mania, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[])
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
let mut gradual = ManiaGradualDifficulty::new(&difficulty, converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
|
||||
let mut gradual = ManiaGradualDifficulty::new(&difficulty, converted.as_owned());
|
||||
let mut gradual_2nd = ManiaGradualDifficulty::new(&difficulty, converted.as_owned());
|
||||
let mut gradual_3rd = ManiaGradualDifficulty::new(&difficulty, converted.as_owned());
|
||||
|
||||
let hit_objects_len = converted.map.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = ModeDifficulty::new()
|
||||
.passed_objects(i as u32)
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-19
@@ -9,6 +9,7 @@ use crate::{
|
||||
|
||||
use super::{attributes::ManiaDifficultyAttributes, convert::ManiaBeatmap};
|
||||
|
||||
pub mod gradual;
|
||||
mod object;
|
||||
mod skills;
|
||||
|
||||
@@ -49,31 +50,14 @@ impl DifficultyValues {
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
let mut params = ObjectParams::new(converted.map.as_ref());
|
||||
|
||||
let mut mania_objects = converted
|
||||
let mania_objects = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
||||
.take(take);
|
||||
|
||||
let Some(first) = mania_objects.next() else {
|
||||
return DifficultyValues {
|
||||
strain: Strain::new(total_columns as usize),
|
||||
max_combo: 0,
|
||||
};
|
||||
};
|
||||
|
||||
let n_diff_objects = mania_objects.len();
|
||||
|
||||
let diff_objects_iter = mania_objects.enumerate().scan(first, |last, (i, base)| {
|
||||
let diff_object = ManiaDifficultyObject::new(&base, last, clock_rate, i);
|
||||
*last = base;
|
||||
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(n_diff_objects);
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
let diff_objects = Self::create_difficulty_objects(clock_rate, mania_objects);
|
||||
|
||||
let mut strain = Strain::new(total_columns as usize);
|
||||
|
||||
@@ -90,4 +74,29 @@ impl DifficultyValues {
|
||||
max_combo: params.into_max_combo(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_difficulty_objects(
|
||||
clock_rate: f64,
|
||||
mut mania_objects: impl ExactSizeIterator<Item = ManiaObject>,
|
||||
) -> Box<[ManiaDifficultyObject]> {
|
||||
let Some(first) = mania_objects.next() else {
|
||||
return Box::default();
|
||||
};
|
||||
|
||||
let n_diff_objects = mania_objects.len();
|
||||
|
||||
let diff_objects_iter = mania_objects.enumerate().scan(first, |last, (i, base)| {
|
||||
let diff_object = ManiaDifficultyObject::new(&base, last, clock_rate, i);
|
||||
*last = base;
|
||||
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(n_diff_objects);
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
debug_assert_eq!(n_diff_objects, diff_objects.len());
|
||||
|
||||
diff_objects.into_boxed_slice()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,17 @@ impl Strain {
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self) -> f64 {
|
||||
self.inner.difficulty_value(StrainDecaySkill::DECAY_WEIGHT)
|
||||
Self::static_difficulty_value(self.inner)
|
||||
}
|
||||
|
||||
/// Use [`difficulty_value`] instead whenever possible because
|
||||
/// [`as_difficulty_value`] clones internally.
|
||||
pub fn as_difficulty_value(&self) -> f64 {
|
||||
Self::static_difficulty_value(self.inner.clone())
|
||||
}
|
||||
|
||||
fn static_difficulty_value(skill: StrainDecaySkill) -> f64 {
|
||||
skill.difficulty_value(StrainDecaySkill::DECAY_WEIGHT)
|
||||
}
|
||||
|
||||
const fn curr_strain(&self) -> f64 {
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ use crate::{
|
||||
pub use self::{
|
||||
attributes::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||
convert::ManiaBeatmap,
|
||||
performance::ManiaPerformance,
|
||||
difficulty::gradual::ManiaGradualDifficulty,
|
||||
performance::{gradual::ManiaGradualPerformance, ManiaPerformance},
|
||||
score_state::ManiaScoreState,
|
||||
strains::ManiaStrains,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use crate::{
|
||||
mania::{ManiaBeatmap, ManiaGradualDifficulty},
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
use super::{ManiaPerformanceAttributes, ManiaScoreState};
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!mania map.
|
||||
///
|
||||
/// After each hit object you can call [`next`] and it will return the
|
||||
/// resulting current [`ManiaPerformanceAttributes`]. To process multiple
|
||||
/// objects at once, use [`nth`] 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
|
||||
/// [`ManiaGradualDifficulty`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::mania::{Mania, ManiaGradualPerformance, ManiaScoreState};
|
||||
///
|
||||
/// let converted = Beatmap::from_path()
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted::<Mania>();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut gradual_perf = ManiaGradualPerformance::new(&difficulty, converted);
|
||||
/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 hitresults are 320s
|
||||
/// for _ in 0..10 {
|
||||
/// state.n320 += 1;
|
||||
///
|
||||
/// let performance = gradual_perf.next(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// }
|
||||
///
|
||||
/// // Then comes a miss.
|
||||
/// state.n_misses += 1;
|
||||
/// let performance = gradual_perf.next(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // The next 10 objects will be a mixture of 320s and 100s.
|
||||
/// // Notice how all 10 objects will be processed in one go.
|
||||
/// state.n320 += 3;
|
||||
/// state.n100 += 7;
|
||||
/// // The `nth` method takes a zero-based value.
|
||||
/// let performance = gradual_perf.nth(score, 9).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n_misses = ...
|
||||
/// # */
|
||||
/// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Once the final performance was calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.next(state).is_none());
|
||||
/// ```
|
||||
///
|
||||
/// [`next`]: ManiaGradualPerformance::next
|
||||
/// [`nth`]: ManiaGradualPerformance::nth
|
||||
#[derive(Debug)]
|
||||
pub struct ManiaGradualPerformance<'map> {
|
||||
difficulty: ManiaGradualDifficulty<'map>,
|
||||
}
|
||||
|
||||
impl<'map> ManiaGradualPerformance<'map> {
|
||||
/// Create a new gradual performance calculator for osu!mania maps.
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: ManiaBeatmap<'map>) -> Self {
|
||||
let difficulty = ManiaGradualDifficulty::new(difficulty, converted);
|
||||
|
||||
Self { difficulty }
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the performance attributes
|
||||
/// for the resulting score.
|
||||
pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, 0)
|
||||
}
|
||||
|
||||
/// Process all remaining hit objects and calculate the final performance
|
||||
/// attributes.
|
||||
pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, usize::MAX)
|
||||
}
|
||||
|
||||
/// Process everything up the the next `n`th hit object and calculate the
|
||||
/// performance attributes for the resulting score state.
|
||||
///
|
||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||
/// `n=1` will process 2, and so on.
|
||||
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
|
||||
let performance = self
|
||||
.difficulty
|
||||
.nth(n)?
|
||||
.performance()
|
||||
.state(state)
|
||||
.mods(self.difficulty.mods)
|
||||
.clock_rate(self.difficulty.clock_rate)
|
||||
.passed_objects(self.difficulty.idx as u32)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
mania::{Mania, ManiaPerformance},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let mods = 88; // HDHRDT
|
||||
let difficulty = ModeDifficulty::new().mods(88);
|
||||
|
||||
let mut gradual = ManiaGradualPerformance::new(&difficulty, converted.as_owned());
|
||||
let mut gradual_2nd = ManiaGradualPerformance::new(&difficulty, converted.as_owned());
|
||||
let mut gradual_3rd = ManiaGradualPerformance::new(&difficulty, converted.as_owned());
|
||||
|
||||
let mut state = ManiaScoreState::default();
|
||||
|
||||
let hit_objects_len = converted.map.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
state.n_misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state.clone()) else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last(state.clone()).is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let mut regular_calc = ManiaPerformance::new(converted.as_owned())
|
||||
.mods(mods)
|
||||
.passed_objects(i as u32)
|
||||
.state(state.clone());
|
||||
|
||||
let regular_state = regular_calc.generate_state();
|
||||
assert_eq!(state, regular_state);
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ use super::{
|
||||
Mania,
|
||||
};
|
||||
|
||||
pub mod gradual;
|
||||
|
||||
/// Performance calculator on osu!mania maps.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[must_use]
|
||||
Reference in New Issue
Block a user