Skip to main content

rosu_pp/any/performance/
gradual.rs

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