rosu_pp/any/performance/
gradual.rs

1use rosu_map::section::general::GameMode;
2
3use crate::{
4    any::{PerformanceAttributes, ScoreState},
5    catch::{Catch, CatchGradualPerformance},
6    mania::{Mania, ManiaGradualPerformance},
7    model::mode::{ConvertError, IGameMode},
8    osu::{Osu, OsuGradualPerformance},
9    taiko::{Taiko, TaikoGradualPerformance},
10    Beatmap, Difficulty,
11};
12
13/// Gradually calculate the performance attributes on maps of any mode.
14///
15/// After each hit object you can call [`next`] and it will return the
16/// resulting current [`PerformanceAttributes`]. To process multiple objects at
17/// the once, use [`nth`] instead.
18///
19/// Both methods require a [`ScoreState`] that contains the current hitresults
20/// as well as the maximum combo so far. Since the map could have any mode, all
21/// fields of `ScoreState` could be of use and should be updated properly.
22///
23/// Alternatively, you can match on the map's mode yourself and use the gradual
24/// performance attribute struct for the corresponding mode, i.e.
25/// [`OsuGradualPerformance`], [`TaikoGradualPerformance`],
26/// [`CatchGradualPerformance`], or [`ManiaGradualPerformance`].
27///
28/// If you only want to calculate difficulty attributes use [`GradualDifficulty`] instead.
29///
30/// # Example
31///
32/// ```
33/// use rosu_pp::{Beatmap, GradualPerformance, Difficulty, any::ScoreState};
34///
35/// let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
36/// let difficulty = Difficulty::new().mods(64); // DT
37/// let mut gradual = GradualPerformance::new(difficulty, &map);
38/// let mut state = ScoreState::new(); // empty state, everything is on 0.
39///
40/// // The first 10 hitresults are 300s
41/// for _ in 0..10 {
42///     state.n300 += 1;
43///     state.max_combo += 1;
44///
45///     let attrs = gradual.next(state.clone()).unwrap();
46///     println!("PP: {}", attrs.pp());
47/// }
48///
49/// // Then comes a miss.
50/// // Note that state's max combo won't be incremented for
51/// // the next few objects because the combo is reset.
52/// state.misses += 1;
53///
54/// let attrs = gradual.next(state.clone()).unwrap();
55/// println!("PP: {}", attrs.pp());
56///
57/// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
58/// // Notice how all 10 objects will be processed in one go.
59/// state.n300 += 2;
60/// state.n100 += 7;
61/// state.n50 += 1;
62///
63/// // The `nth` method takes a zero-based value.
64/// let attrs = gradual.nth(state.clone(), 9).unwrap();
65/// println!("PP: {}", attrs.pp());
66///
67/// // Now comes another 300. Note that the max combo gets incremented again.
68/// state.n300 += 1;
69/// state.max_combo += 1;
70///
71/// let attrs = gradual.next(state.clone()).unwrap();
72/// println!("PP: {}", attrs.pp());
73///
74/// // Skip to the end
75/// # /*
76/// state.max_combo = ...
77/// state.n300 = ...
78/// ...
79/// # */
80/// let attrs = gradual.last(state.clone()).unwrap();
81/// println!("PP: {}", attrs.pp());
82///
83/// // Once the final performance has been calculated, attempting to process
84/// // further objects will return `None`.
85/// assert!(gradual.next(state).is_none());
86/// ```
87///
88/// [`next`]: GradualPerformance::next
89/// [`nth`]: GradualPerformance::nth
90/// [`GradualDifficulty`]: crate::GradualDifficulty
91// 504 vs 184 bytes is an acceptable difference and the Osu variant (424 bytes)
92// is likely the most used one anyway.
93#[allow(clippy::large_enum_variant)]
94pub enum GradualPerformance {
95    Osu(OsuGradualPerformance),
96    Taiko(TaikoGradualPerformance),
97    Catch(CatchGradualPerformance),
98    Mania(ManiaGradualPerformance),
99}
100
101impl GradualPerformance {
102    /// Create a [`GradualPerformance`] for a map of any mode.
103    #[allow(clippy::missing_panics_doc)]
104    pub fn new(difficulty: Difficulty, map: &Beatmap) -> Self {
105        Self::new_with_mode(difficulty, map, map.mode).expect("no conversion required")
106    }
107
108    /// Create a [`GradualPerformance`] for a [`Beatmap`] on a specific [`GameMode`].
109    pub fn new_with_mode(
110        difficulty: Difficulty,
111        map: &Beatmap,
112        mode: GameMode,
113    ) -> Result<Self, ConvertError> {
114        match mode {
115            GameMode::Osu => Osu::gradual_performance(difficulty, map).map(Self::Osu),
116            GameMode::Taiko => Taiko::gradual_performance(difficulty, map).map(Self::Taiko),
117            GameMode::Catch => Catch::gradual_performance(difficulty, map).map(Self::Catch),
118            GameMode::Mania => Mania::gradual_performance(difficulty, map).map(Self::Mania),
119        }
120    }
121
122    /// Process the next hit object and calculate the performance attributes
123    /// for the resulting score state.
124    pub fn next(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
125        self.nth(state, 0)
126    }
127
128    /// Process all remaining hit objects and calculate the final performance
129    /// attributes.
130    pub fn last(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
131        self.nth(state, usize::MAX)
132    }
133
134    /// Process everything up to the next `n`th hitobject and calculate the
135    /// performance attributes for the resulting score state.
136    ///
137    /// Note that the count is zero-indexed, so `n=0` will process 1 object,
138    /// `n=1` will process 2, and so on.
139    pub fn nth(&mut self, state: ScoreState, n: usize) -> Option<PerformanceAttributes> {
140        match self {
141            GradualPerformance::Osu(gradual) => {
142                gradual.nth(state.into(), n).map(PerformanceAttributes::Osu)
143            }
144            GradualPerformance::Taiko(gradual) => gradual
145                .nth(state.into(), n)
146                .map(PerformanceAttributes::Taiko),
147            GradualPerformance::Catch(gradual) => gradual
148                .nth(state.into(), n)
149                .map(PerformanceAttributes::Catch),
150            GradualPerformance::Mania(gradual) => gradual
151                .nth(state.into(), n)
152                .map(PerformanceAttributes::Mania),
153        }
154    }
155
156    /// Returns the amount of remaining objects.
157    #[allow(clippy::len_without_is_empty)]
158    pub fn len(&self) -> usize {
159        match self {
160            GradualPerformance::Osu(gradual) => gradual.len(),
161            GradualPerformance::Taiko(gradual) => gradual.len(),
162            GradualPerformance::Catch(gradual) => gradual.len(),
163            GradualPerformance::Mania(gradual) => gradual.len(),
164        }
165    }
166}