rosu_pp/any/performance/
mod.rs

1use rosu_map::section::general::GameMode;
2
3use crate::{
4    catch::CatchPerformance, mania::ManiaPerformance, osu::OsuPerformance, taiko::TaikoPerformance,
5    Difficulty, GameMods,
6};
7
8use self::into::IntoPerformance;
9
10use super::{attributes::PerformanceAttributes, score_state::ScoreState};
11
12pub mod gradual;
13pub mod into;
14
15/// Performance calculator on maps of any mode.
16#[derive(Clone, Debug, PartialEq)]
17#[must_use]
18pub enum Performance<'map> {
19    Osu(OsuPerformance<'map>),
20    Taiko(TaikoPerformance<'map>),
21    Catch(CatchPerformance<'map>),
22    Mania(ManiaPerformance<'map>),
23}
24
25impl<'map> Performance<'map> {
26    /// Create a new performance calculator for any mode.
27    ///
28    /// The argument `map_or_attrs` must be either
29    /// - previously calculated attributes ([`DifficultyAttributes`],
30    ///   [`PerformanceAttributes`], or mode-specific attributes like
31    ///   [`TaikoDifficultyAttributes`], [`ManiaPerformanceAttributes`], ...)
32    /// - a [`Beatmap`] (by reference or value)
33    ///
34    /// If a map is given, difficulty attributes will need to be calculated
35    /// internally which is a costly operation. Hence, passing attributes
36    /// should be prefered.
37    ///
38    /// However, when passing previously calculated attributes, make sure they
39    /// have been calculated for the same map and [`Difficulty`] settings.
40    /// Otherwise, the final attributes will be incorrect.
41    ///
42    /// [`Beatmap`]: crate::model::beatmap::Beatmap
43    /// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
44    /// [`TaikoDifficultyAttributes`]: crate::taiko::TaikoDifficultyAttributes
45    /// [`ManiaPerformanceAttributes`]: crate::mania::ManiaPerformanceAttributes
46    pub fn new(map_or_attrs: impl IntoPerformance<'map>) -> Self {
47        map_or_attrs.into_performance()
48    }
49
50    /// Consume the performance calculator and calculate
51    /// performance attributes for the given parameters.
52    #[allow(clippy::missing_panics_doc)]
53    pub fn calculate(self) -> PerformanceAttributes {
54        match self {
55            Self::Osu(o) => {
56                PerformanceAttributes::Osu(o.calculate().expect("no conversion required"))
57            }
58            Self::Taiko(t) => {
59                PerformanceAttributes::Taiko(t.calculate().expect("no conversion required"))
60            }
61            Self::Catch(f) => {
62                PerformanceAttributes::Catch(f.calculate().expect("no conversion required"))
63            }
64            Self::Mania(m) => {
65                PerformanceAttributes::Mania(m.calculate().expect("no conversion required"))
66            }
67        }
68    }
69
70    /// Attempt to convert the map to the specified mode.
71    ///
72    /// Returns `Err(self)` if the conversion is incompatible or no beatmap is
73    /// contained, i.e. if this [`Performance`] was created through attributes
74    /// or [`Performance::generate_state`] was called.
75    ///
76    /// If the given mode should be ignored in case of an error, use
77    /// [`mode_or_ignore`] instead.
78    ///
79    /// [`mode_or_ignore`]: Self::mode_or_ignore
80    // Both variants have the same size
81    #[allow(clippy::result_large_err)]
82    pub fn try_mode(self, mode: GameMode) -> Result<Self, Self> {
83        match (self, mode) {
84            (Self::Osu(o), _) => o.try_mode(mode).map_err(Self::Osu),
85            (this @ Self::Taiko(_), GameMode::Taiko)
86            | (this @ Self::Catch(_), GameMode::Catch)
87            | (this @ Self::Mania(_), GameMode::Mania) => Ok(this),
88            (this, _) => Err(this),
89        }
90    }
91
92    /// Attempt to convert the map to the specified mode.
93    ///
94    /// If the conversion is incompatible or if the internal beatmap was
95    /// already replaced with difficulty attributes, the map won't be modified.
96    ///
97    /// To see whether the given mode is incompatible or the internal beatmap
98    /// was replaced, use [`try_mode`] instead.
99    ///
100    /// [`try_mode`]: Self::try_mode
101    pub fn mode_or_ignore(self, mode: GameMode) -> Self {
102        if let Self::Osu(osu) = self {
103            osu.mode_or_ignore(mode)
104        } else {
105            self
106        }
107    }
108
109    /// Specify mods.
110    ///
111    /// Accepted types are
112    /// - `u32`
113    /// - [`rosu_mods::GameModsLegacy`]
114    /// - [`rosu_mods::GameMods`]
115    /// - [`rosu_mods::GameModsIntermode`]
116    /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
117    ///
118    /// See <https://github.com/ppy/osu-api/wiki#mods>
119    pub fn mods(self, mods: impl Into<GameMods>) -> Self {
120        match self {
121            Self::Osu(o) => Self::Osu(o.mods(mods)),
122            Self::Taiko(t) => Self::Taiko(t.mods(mods)),
123            Self::Catch(f) => Self::Catch(f.mods(mods)),
124            Self::Mania(m) => Self::Mania(m.mods(mods)),
125        }
126    }
127
128    /// Use the specified settings of the given [`Difficulty`].
129    pub fn difficulty(self, difficulty: Difficulty) -> Self {
130        match self {
131            Self::Osu(o) => Self::Osu(o.difficulty(difficulty)),
132            Self::Taiko(t) => Self::Taiko(t.difficulty(difficulty)),
133            Self::Catch(f) => Self::Catch(f.difficulty(difficulty)),
134            Self::Mania(m) => Self::Mania(m.difficulty(difficulty)),
135        }
136    }
137
138    /// Amount of passed objects for partial plays, e.g. a fail.
139    ///
140    /// If you want to calculate the performance after every few objects,
141    /// instead of using [`Performance`] multiple times with different
142    /// `passed_objects`, you should use [`GradualPerformance`].
143    ///
144    /// [`GradualPerformance`]: crate::GradualPerformance
145    pub fn passed_objects(self, passed_objects: u32) -> Self {
146        match self {
147            Self::Osu(o) => Self::Osu(o.passed_objects(passed_objects)),
148            Self::Taiko(t) => Self::Taiko(t.passed_objects(passed_objects)),
149            Self::Catch(f) => Self::Catch(f.passed_objects(passed_objects)),
150            Self::Mania(m) => Self::Mania(m.passed_objects(passed_objects)),
151        }
152    }
153
154    /// Adjust the clock rate used in the calculation.
155    ///
156    /// If none is specified, it will take the clock rate based on the mods
157    /// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
158    ///
159    /// | Minimum | Maximum |
160    /// | :-----: | :-----: |
161    /// | 0.01    | 100     |
162    pub fn clock_rate(self, clock_rate: f64) -> Self {
163        match self {
164            Self::Osu(o) => Self::Osu(o.clock_rate(clock_rate)),
165            Self::Taiko(t) => Self::Taiko(t.clock_rate(clock_rate)),
166            Self::Catch(f) => Self::Catch(f.clock_rate(clock_rate)),
167            Self::Mania(m) => Self::Mania(m.clock_rate(clock_rate)),
168        }
169    }
170
171    /// Override a beatmap's set AR.
172    ///
173    /// Only relevant for osu! and osu!catch.
174    ///
175    /// `with_mods` determines if the given value should be used before
176    /// or after accounting for mods, e.g. on `true` the value will be
177    /// used as is and on `false` it will be modified based on the mods.
178    ///
179    /// | Minimum | Maximum |
180    /// | :-----: | :-----: |
181    /// | -20     | 20      |
182    pub fn ar(self, ar: f32, with_mods: bool) -> Self {
183        match self {
184            Self::Osu(o) => Self::Osu(o.ar(ar, with_mods)),
185            Self::Catch(c) => Self::Catch(c.ar(ar, with_mods)),
186            Self::Taiko(_) | Self::Mania(_) => self,
187        }
188    }
189
190    /// Override a beatmap's set CS.
191    ///
192    /// Only relevant for osu! and osu!catch.
193    ///
194    /// `with_mods` determines if the given value should be used before
195    /// or after accounting for mods, e.g. on `true` the value will be
196    /// used as is and on `false` it will be modified based on the mods.
197    ///
198    /// | Minimum | Maximum |
199    /// | :-----: | :-----: |
200    /// | -20     | 20      |
201    pub fn cs(self, cs: f32, with_mods: bool) -> Self {
202        match self {
203            Self::Osu(o) => Self::Osu(o.cs(cs, with_mods)),
204            Self::Catch(c) => Self::Catch(c.cs(cs, with_mods)),
205            Self::Taiko(_) | Self::Mania(_) => self,
206        }
207    }
208
209    /// Override a beatmap's set HP.
210    ///
211    /// `with_mods` determines if the given value should be used before
212    /// or after accounting for mods, e.g. on `true` the value will be
213    /// used as is and on `false` it will be modified based on the mods.
214    ///
215    /// | Minimum | Maximum |
216    /// | :-----: | :-----: |
217    /// | -20     | 20      |
218    pub fn hp(self, hp: f32, with_mods: bool) -> Self {
219        match self {
220            Self::Osu(o) => Self::Osu(o.hp(hp, with_mods)),
221            Self::Taiko(t) => Self::Taiko(t.hp(hp, with_mods)),
222            Self::Catch(c) => Self::Catch(c.hp(hp, with_mods)),
223            Self::Mania(m) => Self::Mania(m.hp(hp, with_mods)),
224        }
225    }
226
227    /// Override a beatmap's set OD.
228    ///
229    /// `with_mods` determines if the given value should be used before
230    /// or after accounting for mods, e.g. on `true` the value will be
231    /// used as is and on `false` it will be modified based on the mods.
232    ///
233    /// | Minimum | Maximum |
234    /// | :-----: | :-----: |
235    /// | -20     | 20      |
236    pub fn od(self, od: f32, with_mods: bool) -> Self {
237        match self {
238            Self::Osu(o) => Self::Osu(o.od(od, with_mods)),
239            Self::Taiko(t) => Self::Taiko(t.od(od, with_mods)),
240            Self::Catch(c) => Self::Catch(c.od(od, with_mods)),
241            Self::Mania(m) => Self::Mania(m.od(od, with_mods)),
242        }
243    }
244
245    /// Adjust patterns as if the HR mod is enabled.
246    ///
247    /// Only relevant for osu!catch.
248    pub fn hardrock_offsets(self, hardrock_offsets: bool) -> Self {
249        if let Self::Catch(catch) = self {
250            Self::Catch(catch.hardrock_offsets(hardrock_offsets))
251        } else {
252            self
253        }
254    }
255
256    /// Provide parameters through a [`ScoreState`].
257    pub fn state(self, state: ScoreState) -> Self {
258        match self {
259            Self::Osu(o) => Self::Osu(o.state(state.into())),
260            Self::Taiko(t) => Self::Taiko(t.state(state.into())),
261            Self::Catch(f) => Self::Catch(f.state(state.into())),
262            Self::Mania(m) => Self::Mania(m.state(state.into())),
263        }
264    }
265
266    /// Set the accuracy between `0.0` and `100.0`.
267    pub fn accuracy(self, acc: f64) -> Self {
268        match self {
269            Self::Osu(o) => Self::Osu(o.accuracy(acc)),
270            Self::Taiko(t) => Self::Taiko(t.accuracy(acc)),
271            Self::Catch(f) => Self::Catch(f.accuracy(acc)),
272            Self::Mania(m) => Self::Mania(m.accuracy(acc)),
273        }
274    }
275
276    /// Specify the amount of misses of a play.
277    pub fn misses(self, n_misses: u32) -> Self {
278        match self {
279            Self::Osu(o) => Self::Osu(o.misses(n_misses)),
280            Self::Taiko(t) => Self::Taiko(t.misses(n_misses)),
281            Self::Catch(f) => Self::Catch(f.misses(n_misses)),
282            Self::Mania(m) => Self::Mania(m.misses(n_misses)),
283        }
284    }
285
286    /// Specify the max combo of the play.
287    ///
288    /// Irrelevant for osu!mania.
289    pub fn combo(self, combo: u32) -> Self {
290        match self {
291            Self::Osu(o) => Self::Osu(o.combo(combo)),
292            Self::Taiko(t) => Self::Taiko(t.combo(combo)),
293            Self::Catch(f) => Self::Catch(f.combo(combo)),
294            Self::Mania(_) => self,
295        }
296    }
297
298    /// Specify how hitresults should be generated.
299    ///
300    /// Defauls to [`HitResultPriority::BestCase`].
301    pub fn hitresult_priority(self, priority: HitResultPriority) -> Self {
302        match self {
303            Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
304            Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
305            Self::Catch(_) => self,
306            Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
307        }
308    }
309
310    /// Whether the calculated attributes belong to an osu!lazer or osu!stable
311    /// score.
312    ///
313    /// Defaults to `true`.
314    ///
315    /// This affects internal accuracy calculation because lazer considers
316    /// slider heads for accuracy whereas stable does not.
317    ///
318    /// Only relevant for osu!standard and osu!mania.
319    pub fn lazer(self, lazer: bool) -> Self {
320        match self {
321            Self::Osu(o) => Self::Osu(o.lazer(lazer)),
322            Self::Taiko(_) | Self::Catch(_) => self,
323            Self::Mania(m) => Self::Mania(m.lazer(lazer)),
324        }
325    }
326
327    /// Specify the amount of "large tick" hits.
328    ///
329    /// Only relevant for osu!standard.
330    ///
331    /// The meaning depends on the kind of score:
332    /// - if set on osu!stable, this value is irrelevant and can be `0`
333    /// - if set on osu!lazer *with* slider accuracy, this value is the amount
334    ///   of hit slider ticks and repeats
335    /// - if set on osu!lazer *without* slider accuracy, this value is the
336    ///   amount of hit slider heads, ticks, and repeats
337    pub fn large_tick_hits(self, large_tick_hits: u32) -> Self {
338        if let Self::Osu(osu) = self {
339            Self::Osu(osu.large_tick_hits(large_tick_hits))
340        } else {
341            self
342        }
343    }
344
345    /// Specify the amount of "small tick" hits.
346    ///
347    /// Only relevant for osu!standard lazer scores without slider accuracy. In
348    /// that case, this value is the amount of slider tail hits.
349    pub fn small_tick_hits(self, small_tick_hits: u32) -> Self {
350        if let Self::Osu(osu) = self {
351            Self::Osu(osu.small_tick_hits(small_tick_hits))
352        } else {
353            self
354        }
355    }
356
357    /// Specify the amount of hit slider ends.
358    ///
359    /// Only relevant for osu!standard lazer scores with slider accuracy.
360    pub fn slider_end_hits(self, slider_end_hits: u32) -> Self {
361        if let Self::Osu(osu) = self {
362            Self::Osu(osu.slider_end_hits(slider_end_hits))
363        } else {
364            self
365        }
366    }
367
368    /// Specify the amount of 300s of a play.
369    pub fn n300(self, n300: u32) -> Self {
370        match self {
371            Self::Osu(o) => Self::Osu(o.n300(n300)),
372            Self::Taiko(t) => Self::Taiko(t.n300(n300)),
373            Self::Catch(f) => Self::Catch(f.fruits(n300)),
374            Self::Mania(m) => Self::Mania(m.n300(n300)),
375        }
376    }
377
378    /// Specify the amount of 100s of a play.
379    pub fn n100(self, n100: u32) -> Self {
380        match self {
381            Self::Osu(o) => Self::Osu(o.n100(n100)),
382            Self::Taiko(t) => Self::Taiko(t.n100(n100)),
383            Self::Catch(f) => Self::Catch(f.droplets(n100)),
384            Self::Mania(m) => Self::Mania(m.n100(n100)),
385        }
386    }
387
388    /// Specify the amount of 50s of a play.
389    ///
390    /// Irrelevant for osu!taiko.
391    pub fn n50(self, n50: u32) -> Self {
392        match self {
393            Self::Osu(o) => Self::Osu(o.n50(n50)),
394            Self::Taiko(_) => self,
395            Self::Catch(f) => Self::Catch(f.tiny_droplets(n50)),
396            Self::Mania(m) => Self::Mania(m.n50(n50)),
397        }
398    }
399
400    /// Specify the amount of katus of a play.
401    ///
402    /// Only relevant for osu!catch for which it represents the amount of tiny
403    /// droplet misses and osu!mania for which it repesents the amount of n200.
404    pub fn n_katu(self, n_katu: u32) -> Self {
405        match self {
406            Self::Osu(_) | Self::Taiko(_) => self,
407            Self::Catch(f) => Self::Catch(f.tiny_droplet_misses(n_katu)),
408            Self::Mania(m) => Self::Mania(m.n200(n_katu)),
409        }
410    }
411
412    /// Specify the amount of gekis of a play.
413    ///
414    /// Only relevant for osu!mania for which it repesents the
415    /// amount of n320.
416    pub fn n_geki(self, n_geki: u32) -> Self {
417        match self {
418            Self::Osu(_) | Self::Taiko(_) | Self::Catch(_) => self,
419            Self::Mania(m) => Self::Mania(m.n320(n_geki)),
420        }
421    }
422
423    /// Create the [`ScoreState`] that will be used for performance calculation.
424    #[allow(clippy::missing_panics_doc)]
425    pub fn generate_state(&mut self) -> ScoreState {
426        match self {
427            Self::Osu(o) => o.generate_state().expect("no conversion required").into(),
428            Self::Taiko(t) => t.generate_state().expect("no conversion required").into(),
429            Self::Catch(f) => f.generate_state().expect("no conversion required").into(),
430            Self::Mania(m) => m.generate_state().expect("no conversion required").into(),
431        }
432    }
433}
434
435/// While generating remaining hitresults, decide how they should be distributed.
436#[derive(Copy, Clone, Debug, Eq, PartialEq)]
437#[non_exhaustive]
438pub enum HitResultPriority {
439    /// Prioritize good hitresults over bad ones
440    BestCase,
441    /// Prioritize bad hitresults over good ones
442    WorstCase,
443    /// Prioritize fast hitresults generation
444    Fastest,
445}
446
447impl HitResultPriority {
448    pub(crate) const DEFAULT: Self = Self::BestCase;
449}
450
451impl Default for HitResultPriority {
452    fn default() -> Self {
453        Self::DEFAULT
454    }
455}
456
457impl<'a, T: IntoPerformance<'a>> From<T> for Performance<'a> {
458    fn from(into: T) -> Self {
459        into.into_performance()
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use crate::{
466        any::DifficultyAttributes,
467        catch::{CatchDifficultyAttributes, CatchPerformanceAttributes},
468        mania::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
469        osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
470        taiko::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
471        Beatmap,
472    };
473
474    use super::*;
475
476    #[test]
477    fn create() {
478        let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
479
480        let _ = Performance::new(&map);
481        let _ = Performance::new(map.clone());
482
483        let _ = Performance::new(OsuDifficultyAttributes::default());
484        let _ = Performance::new(TaikoDifficultyAttributes::default());
485        let _ = Performance::new(CatchDifficultyAttributes::default());
486        let _ = Performance::new(ManiaDifficultyAttributes::default());
487
488        let _ = Performance::new(OsuPerformanceAttributes::default());
489        let _ = Performance::new(TaikoPerformanceAttributes::default());
490        let _ = Performance::new(CatchPerformanceAttributes::default());
491        let _ = Performance::new(ManiaPerformanceAttributes::default());
492
493        let _ = Performance::new(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
494        let _ = Performance::new(PerformanceAttributes::Taiko(
495            TaikoPerformanceAttributes::default(),
496        ));
497
498        let _ = Performance::from(&map);
499        let _ = Performance::from(map);
500
501        let _ = Performance::from(OsuDifficultyAttributes::default());
502        let _ = Performance::from(TaikoDifficultyAttributes::default());
503        let _ = Performance::from(CatchDifficultyAttributes::default());
504        let _ = Performance::from(ManiaDifficultyAttributes::default());
505
506        let _ = Performance::from(OsuPerformanceAttributes::default());
507        let _ = Performance::from(TaikoPerformanceAttributes::default());
508        let _ = Performance::from(CatchPerformanceAttributes::default());
509        let _ = Performance::from(ManiaPerformanceAttributes::default());
510
511        let _ = Performance::from(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
512        let _ = Performance::from(PerformanceAttributes::Taiko(
513            TaikoPerformanceAttributes::default(),
514        ));
515
516        let _ = DifficultyAttributes::Osu(OsuDifficultyAttributes::default()).performance();
517        let _ = PerformanceAttributes::Taiko(TaikoPerformanceAttributes::default()).performance();
518    }
519}