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}