add gradual calc for any mode

This commit is contained in:
MaxOhn
2024-02-26 16:15:08 +01:00
parent 29ee3147cc
commit 8d2132c124
6 changed files with 312 additions and 8 deletions
+130
View File
@@ -0,0 +1,130 @@
use std::borrow::Cow;
use rosu_map::section::general::GameMode;
use crate::{
any::DifficultyAttributes,
catch::{CatchBeatmap, CatchGradualDifficulty},
mania::{ManiaBeatmap, ManiaGradualDifficulty},
osu::{OsuBeatmap, OsuGradualDifficulty},
taiko::{TaikoBeatmap, TaikoGradualDifficulty},
Beatmap, Converted, ModeDifficulty,
};
/// Gradually calculate the difficulty attributes on maps of any mode.
///
/// Note that this type implements [`Iterator`]. On every call of
/// [`Iterator::next`], the next object will be processed and the
/// [`DifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use [`GradualPerformance`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualDifficulty, ModeDifficulty};
///
/// let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let mut iter = GradualDifficulty::new(&difficulty, &map);
///
/// // the difficulty of the map after the first object
/// let attrs1 = iter.next();
/// // ... after the second object
/// let attrs2 = iter.next();
///
/// // Remaining objects
/// for difficulty in iter {
/// // ...
/// }
/// ```
///
/// [`GradualPerformance`]: crate::GradualPerformance
pub enum GradualDifficulty {
Osu(OsuGradualDifficulty),
Taiko(TaikoGradualDifficulty),
Catch(CatchGradualDifficulty),
Mania(ManiaGradualDifficulty),
}
macro_rules! from_converted {
( $fn:ident, $mode:ident, $converted:ident, $gradual:ident ) => {
#[doc = concat!("Create a [`GradualDifficulty`] for a [`", stringify!($converted), "`]")]
pub fn $fn(difficulty: &ModeDifficulty, converted: &$converted<'_>) -> Self {
Self::$mode($gradual::new(difficulty, converted))
}
};
}
impl GradualDifficulty {
/// Create a [`GradualDifficulty`] for a map of any mode.
pub fn new(difficulty: &ModeDifficulty, map: &Beatmap) -> Self {
let map = Cow::Borrowed(map);
match map.mode {
GameMode::Osu => Self::Osu(OsuGradualDifficulty::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Taiko => Self::Taiko(TaikoGradualDifficulty::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Catch => Self::Catch(CatchGradualDifficulty::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Mania => Self::Mania(ManiaGradualDifficulty::new(
difficulty,
&Converted::new(map, false),
)),
}
}
from_converted!(from_osu_map, Osu, OsuBeatmap, OsuGradualDifficulty);
from_converted!(from_taiko_map, Taiko, TaikoBeatmap, TaikoGradualDifficulty);
from_converted!(from_catch_map, Catch, CatchBeatmap, CatchGradualDifficulty);
from_converted!(from_mania_map, Mania, ManiaBeatmap, ManiaGradualDifficulty);
}
impl Iterator for GradualDifficulty {
type Item = DifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
match self {
GradualDifficulty::Osu(gradual) => gradual.next().map(DifficultyAttributes::Osu),
GradualDifficulty::Taiko(gradual) => gradual.next().map(DifficultyAttributes::Taiko),
GradualDifficulty::Catch(gradual) => gradual.next().map(DifficultyAttributes::Catch),
GradualDifficulty::Mania(gradual) => gradual.next().map(DifficultyAttributes::Mania),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
GradualDifficulty::Osu(gradual) => gradual.size_hint(),
GradualDifficulty::Taiko(gradual) => gradual.size_hint(),
GradualDifficulty::Catch(gradual) => gradual.size_hint(),
GradualDifficulty::Mania(gradual) => gradual.size_hint(),
}
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
GradualDifficulty::Osu(gradual) => gradual.nth(n).map(DifficultyAttributes::Osu),
GradualDifficulty::Taiko(gradual) => gradual.nth(n).map(DifficultyAttributes::Taiko),
GradualDifficulty::Catch(gradual) => gradual.nth(n).map(DifficultyAttributes::Catch),
GradualDifficulty::Mania(gradual) => gradual.nth(n).map(DifficultyAttributes::Mania),
}
}
}
impl ExactSizeIterator for GradualDifficulty {
fn len(&self) -> usize {
match self {
GradualDifficulty::Osu(gradual) => gradual.len(),
GradualDifficulty::Taiko(gradual) => gradual.len(),
GradualDifficulty::Catch(gradual) => gradual.len(),
GradualDifficulty::Mania(gradual) => gradual.len(),
}
}
}
+1
View File
@@ -14,6 +14,7 @@ use self::mode::ModeDifficulty;
use super::attributes::DifficultyAttributes;
pub mod gradual;
pub mod mode;
pub mod object;
pub mod skills;
+2 -2
View File
@@ -2,8 +2,8 @@ pub use self::{
attributes::{
AttributeProvider, DifficultyAttributes, ModeAttributeProvider, PerformanceAttributes,
},
difficulty::{mode::ModeDifficulty, Difficulty},
performance::{HitResultPriority, Performance},
difficulty::{gradual::GradualDifficulty, mode::ModeDifficulty, Difficulty},
performance::{gradual::GradualPerformance, HitResultPriority, Performance},
score_state::ScoreState,
};
+171
View File
@@ -0,0 +1,171 @@
use std::borrow::Cow;
use rosu_map::section::general::GameMode;
use crate::{
any::{PerformanceAttributes, ScoreState},
catch::{CatchBeatmap, CatchGradualPerformance},
mania::{ManiaBeatmap, ManiaGradualPerformance},
osu::{OsuBeatmap, OsuGradualPerformance},
taiko::{TaikoBeatmap, TaikoGradualPerformance},
Beatmap, Converted, ModeDifficulty,
};
/// Gradually calculate the performance attributes on maps of any mode.
///
/// After each hit object you can call [`next`] and it will return the
/// resulting current [`PerformanceAttributes`]. To process multiple objects at
/// the once, use [`nth`] instead.
///
/// Both methods require a [`ScoreState`] that contains the current hitresults
/// as well as the maximum combo so far. Since the map could have any mode, all
/// fields of `ScoreState` could be of use and should be updated properly.
///
/// Alternatively, you can match on the map's mode yourself and use the gradual
/// performance attribute struct for the corresponding mode, i.e.
/// [`OsuGradualPerformance`], [`TaikoGradualPerformance`],
/// [`CatchGradualPerformance`], or [`ManiaGradualPerformance`].
///
/// If you only want to calculate difficulty attributes use [`GradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualPerformance, ModeDifficulty, ScoreState};
///
/// let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let mut gradual_perf = GradualPerformance::new(&difficulty, &map);
/// let mut state = ScoreState::new(); // empty state, everything is on 0.
///
/// // The first 10 hitresults are 300s
/// for _ in 0..10 {
/// state.n300 += 1;
/// state.max_combo += 1;
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
/// }
///
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.n_misses += 1;
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2;
/// state.n100 += 7;
/// state.n50 += 1;
///
/// // The `nth` method takes a zero-based value.
/// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1;
/// state.max_combo += 1;
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // Skip to the end
/// # /*
/// state.max_combo = ...
/// state.n300 = ...
/// ...
/// # */
/// let final_performance = gradual_perf.last(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // Once the final performance has been calculated, attempting to process
/// // further objects will return `None`.
/// assert!(gradual_perf.next(state).is_none());
/// ```
///
/// [`next`]: GradualPerformance::next
/// [`nth`]: GradualPerformance::nth
/// [`GradualDifficulty`]: crate::GradualDifficulty
pub enum GradualPerformance {
Osu(OsuGradualPerformance),
Taiko(TaikoGradualPerformance),
Catch(CatchGradualPerformance),
Mania(ManiaGradualPerformance),
}
macro_rules! from_converted {
( $fn:ident, $mode:ident, $converted:ident, $gradual:ident ) => {
#[doc = concat!("Create a [`GradualPerformance`] for a [`", stringify!($converted), "`]")]
pub fn $fn(difficulty: &ModeDifficulty, converted: &$converted<'_>) -> Self {
Self::$mode($gradual::new(difficulty, converted))
}
};
}
impl GradualPerformance {
/// Create a [`GradualPerformance`] for a map of any mode.
pub fn new(difficulty: &ModeDifficulty, map: &Beatmap) -> Self {
let map = Cow::Borrowed(map);
match map.mode {
GameMode::Osu => Self::Osu(OsuGradualPerformance::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Taiko => Self::Taiko(TaikoGradualPerformance::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Catch => Self::Catch(CatchGradualPerformance::new(
difficulty,
&Converted::new(map, false),
)),
GameMode::Mania => Self::Mania(ManiaGradualPerformance::new(
difficulty,
&Converted::new(map, false),
)),
}
}
from_converted!(from_osu_map, Osu, OsuBeatmap, OsuGradualPerformance);
from_converted!(from_taiko_map, Taiko, TaikoBeatmap, TaikoGradualPerformance);
from_converted!(from_catch_map, Catch, CatchBeatmap, CatchGradualPerformance);
from_converted!(from_mania_map, Mania, ManiaBeatmap, ManiaGradualPerformance);
/// Process the next hit object and calculate the performance attributes
/// for the resulting score state.
pub fn next(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance
/// attributes.
pub fn last(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up to the next `n`th hitobject 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: ScoreState, n: usize) -> Option<PerformanceAttributes> {
match self {
GradualPerformance::Osu(gradual) => {
gradual.nth(state.into(), n).map(PerformanceAttributes::Osu)
}
GradualPerformance::Taiko(gradual) => gradual
.nth(state.into(), n)
.map(PerformanceAttributes::Taiko),
GradualPerformance::Catch(gradual) => gradual
.nth(state.into(), n)
.map(PerformanceAttributes::Catch),
GradualPerformance::Mania(gradual) => gradual
.nth(state.into(), n)
.map(PerformanceAttributes::Mania),
}
}
}
@@ -16,6 +16,8 @@ use super::{
score_state::ScoreState,
};
pub mod gradual;
/// Performance calculator on maps of any mode.
#[derive(Clone, Debug, PartialEq)]
#[must_use]
@@ -285,7 +287,7 @@ impl<A: AttributeProvider> From<A> for Performance<'_> {
}
}
macro_rules! impl_from_mode {
macro_rules! impl_from_converted {
( $mode:ident: $performance:ident ) => {
impl<'a> From<Converted<'a, $mode>> for Performance<'a> {
fn from(converted: Converted<'a, $mode>) -> Self {
@@ -301,10 +303,10 @@ macro_rules! impl_from_mode {
};
}
impl_from_mode!(Osu: OsuPerformance);
impl_from_mode!(Taiko: TaikoPerformance);
impl_from_mode!(Catch: CatchPerformance);
impl_from_mode!(Mania: ManiaPerformance);
impl_from_converted!(Osu: OsuPerformance);
impl_from_converted!(Taiko: TaikoPerformance);
impl_from_converted!(Catch: CatchPerformance);
impl_from_converted!(Mania: ManiaPerformance);
/// While generating remaining hitresults, decide how they should be distributed.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+1 -1
View File
@@ -107,7 +107,7 @@
#[doc(inline)]
pub use self::{
any::{Difficulty, ModeDifficulty, Performance},
any::{Difficulty, GradualDifficulty, GradualPerformance, ModeDifficulty, Performance},
model::beatmap::{Beatmap, Converted},
};