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}