rosu_pp/mania/performance/
mod.rs

1use std::cmp;
2
3use rosu_map::section::general::GameMode;
4
5use self::calculator::ManiaPerformanceCalculator;
6
7use crate::{
8    any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
9    model::{mode::ConvertError, mods::GameMods},
10    osu::OsuPerformance,
11    util::map_or_attrs::MapOrAttrs,
12    Performance,
13};
14
15use super::{attributes::ManiaPerformanceAttributes, score_state::ManiaScoreState, Mania};
16
17mod calculator;
18pub mod gradual;
19
20/// Performance calculator on osu!mania maps.
21#[derive(Clone, Debug, PartialEq)]
22#[must_use]
23pub struct ManiaPerformance<'map> {
24    map_or_attrs: MapOrAttrs<'map, Mania>,
25    difficulty: Difficulty,
26    n320: Option<u32>,
27    n300: Option<u32>,
28    n200: Option<u32>,
29    n100: Option<u32>,
30    n50: Option<u32>,
31    misses: Option<u32>,
32    acc: Option<f64>,
33    hitresult_priority: HitResultPriority,
34}
35
36impl<'map> ManiaPerformance<'map> {
37    /// Create a new performance calculator for osu!mania maps.
38    ///
39    /// The argument `map_or_attrs` must be either
40    /// - previously calculated attributes ([`ManiaDifficultyAttributes`]
41    ///   or [`ManiaPerformanceAttributes`])
42    /// - a [`Beatmap`] (by reference or value)
43    ///
44    /// If a map is given, difficulty attributes will need to be calculated
45    /// internally which is a costly operation. Hence, passing attributes
46    /// should be prefered.
47    ///
48    /// However, when passing previously calculated attributes, make sure they
49    /// have been calculated for the same map and [`Difficulty`] settings.
50    /// Otherwise, the final attributes will be incorrect.
51    ///
52    /// [`Beatmap`]: crate::model::beatmap::Beatmap
53    /// [`ManiaDifficultyAttributes`]: crate::mania::ManiaDifficultyAttributes
54    pub fn new(map_or_attrs: impl IntoModePerformance<'map, Mania>) -> Self {
55        map_or_attrs.into_performance()
56    }
57
58    /// Try to create a new performance calculator for osu!mania maps.
59    ///
60    /// Returns `None` if `map_or_attrs` does not belong to osu!mania i.e.
61    /// a [`DifficultyAttributes`] or [`PerformanceAttributes`] of a different
62    /// mode.
63    ///
64    /// See [`ManiaPerformance::new`] for more information.
65    ///
66    /// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
67    /// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
68    pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
69        if let Performance::Mania(calc) = map_or_attrs.into_performance() {
70            Some(calc)
71        } else {
72            None
73        }
74    }
75
76    /// Specify mods.
77    ///
78    /// Accepted types are
79    /// - `u32`
80    /// - [`rosu_mods::GameModsLegacy`]
81    /// - [`rosu_mods::GameMods`]
82    /// - [`rosu_mods::GameModsIntermode`]
83    /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
84    ///
85    /// See <https://github.com/ppy/osu-api/wiki#mods>
86    pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
87        self.difficulty = self.difficulty.mods(mods);
88
89        self
90    }
91
92    /// Use the specified settings of the given [`Difficulty`].
93    pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
94        self.difficulty = difficulty;
95
96        self
97    }
98
99    /// Amount of passed objects for partial plays, e.g. a fail.
100    ///
101    /// If you want to calculate the performance after every few objects,
102    /// instead of using [`ManiaPerformance`] multiple times with different
103    /// `passed_objects`, you should use [`ManiaGradualPerformance`].
104    ///
105    /// [`ManiaGradualPerformance`]: crate::mania::ManiaGradualPerformance
106    pub fn passed_objects(mut self, passed_objects: u32) -> Self {
107        self.difficulty = self.difficulty.passed_objects(passed_objects);
108
109        self
110    }
111
112    /// Adjust the clock rate used in the calculation.
113    ///
114    /// If none is specified, it will take the clock rate based on the mods
115    /// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
116    ///
117    /// | Minimum | Maximum |
118    /// | :-----: | :-----: |
119    /// | 0.01    | 100     |
120    pub fn clock_rate(mut self, clock_rate: f64) -> Self {
121        self.difficulty = self.difficulty.clock_rate(clock_rate);
122
123        self
124    }
125
126    /// Override a beatmap's set HP.
127    ///
128    /// `with_mods` determines if the given value should be used before
129    /// or after accounting for mods, e.g. on `true` the value will be
130    /// used as is and on `false` it will be modified based on the mods.
131    ///
132    /// | Minimum | Maximum |
133    /// | :-----: | :-----: |
134    /// | -20     | 20      |
135    pub fn hp(mut self, hp: f32, with_mods: bool) -> Self {
136        self.difficulty = self.difficulty.hp(hp, with_mods);
137
138        self
139    }
140
141    /// Override a beatmap's set OD.
142    ///
143    /// `with_mods` determines if the given value should be used before
144    /// or after accounting for mods, e.g. on `true` the value will be
145    /// used as is and on `false` it will be modified based on the mods.
146    ///
147    /// | Minimum | Maximum |
148    /// | :-----: | :-----: |
149    /// | -20     | 20      |
150    pub fn od(mut self, od: f32, with_mods: bool) -> Self {
151        self.difficulty = self.difficulty.od(od, with_mods);
152
153        self
154    }
155
156    /// Specify the accuracy of a play between `0.0` and `100.0`.
157    /// This will be used to generate matching hitresults.
158    pub fn accuracy(mut self, acc: f64) -> Self {
159        self.acc = Some(acc.clamp(0.0, 100.0) / 100.0);
160
161        self
162    }
163
164    /// Specify how hitresults should be generated.
165    ///
166    /// Defauls to [`HitResultPriority::BestCase`].
167    pub const fn hitresult_priority(mut self, priority: HitResultPriority) -> Self {
168        self.hitresult_priority = priority;
169
170        self
171    }
172
173    /// Whether the calculated attributes belong to an osu!lazer or osu!stable
174    /// score.
175    ///
176    /// Defaults to `true`.
177    ///
178    /// This affects internal hitresult generation because lazer (without `CL`
179    /// mod) gives two hitresults per hold note whereas stable only gives one.
180    /// It also affect accuracy calculation because stable makes no difference
181    /// between perfect (n320) and great (n300) hitresults but lazer (without
182    /// `CL` mod) rewards slightly more for perfect hitresults.
183    pub fn lazer(mut self, lazer: bool) -> Self {
184        self.difficulty = self.difficulty.lazer(lazer);
185
186        self
187    }
188
189    /// Specify the amount of 320s of a play.
190    pub const fn n320(mut self, n320: u32) -> Self {
191        self.n320 = Some(n320);
192
193        self
194    }
195
196    /// Specify the amount of 300s of a play.
197    pub const fn n300(mut self, n300: u32) -> Self {
198        self.n300 = Some(n300);
199
200        self
201    }
202
203    /// Specify the amount of 200s of a play.
204    pub const fn n200(mut self, n200: u32) -> Self {
205        self.n200 = Some(n200);
206
207        self
208    }
209
210    /// Specify the amount of 100s of a play.
211    pub const fn n100(mut self, n100: u32) -> Self {
212        self.n100 = Some(n100);
213
214        self
215    }
216
217    /// Specify the amount of 50s of a play.
218    pub const fn n50(mut self, n50: u32) -> Self {
219        self.n50 = Some(n50);
220
221        self
222    }
223
224    /// Specify the amount of misses of a play.
225    pub const fn misses(mut self, n_misses: u32) -> Self {
226        self.misses = Some(n_misses);
227
228        self
229    }
230
231    /// Provide parameters through an [`ManiaScoreState`].
232    #[allow(clippy::needless_pass_by_value)]
233    pub const fn state(mut self, state: ManiaScoreState) -> Self {
234        let ManiaScoreState {
235            n320,
236            n300,
237            n200,
238            n100,
239            n50,
240            misses,
241        } = state;
242
243        self.n320 = Some(n320);
244        self.n300 = Some(n300);
245        self.n200 = Some(n200);
246        self.n100 = Some(n100);
247        self.n50 = Some(n50);
248        self.misses = Some(misses);
249
250        self
251    }
252
253    /// Create the [`ManiaScoreState`] that will be used for performance calculation.
254    #[allow(clippy::too_many_lines, clippy::similar_names)]
255    pub fn generate_state(&mut self) -> Result<ManiaScoreState, ConvertError> {
256        let attrs = match self.map_or_attrs {
257            MapOrAttrs::Map(ref map) => {
258                let attrs = self.difficulty.calculate_for_mode::<Mania>(map)?;
259
260                self.map_or_attrs.insert_attrs(attrs)
261            }
262            MapOrAttrs::Attrs(ref attrs) => attrs,
263        };
264
265        let priority = self.hitresult_priority;
266        let mut n_objects = cmp::min(self.difficulty.get_passed_objects() as u32, attrs.n_objects);
267        let misses = self.misses.map_or(0, |n| cmp::min(n, n_objects));
268        let classic = !self.difficulty.get_lazer() || self.difficulty.get_mods().cl();
269
270        if !classic {
271            n_objects += attrs.n_hold_notes;
272        }
273
274        let n_remaining = n_objects - misses;
275
276        let min_remaining = |n: u32| cmp::min(n, n_remaining);
277
278        let mut n320 = self.n320.map_or(0, min_remaining);
279        let mut n300 = self.n300.map_or(0, min_remaining);
280        let mut n200 = self.n200.map_or(0, min_remaining);
281        let mut n100 = self.n100.map_or(0, min_remaining);
282        let mut n50 = self.n50.map_or(0, min_remaining);
283
284        let generate_fast = |acc: f64| {
285            let target = i32::max(
286                0,
287                f64::round(acc * f64::from(if classic { 60 } else { 61 } * n_objects)) as i32,
288            ) as u32;
289
290            let mut remaining_hits = n_remaining;
291            let mut delta = target - 10 * remaining_hits;
292
293            let perfect_factor = if classic { 50 } else { 51 };
294
295            if let Some(n320) = self.n320 {
296                delta = delta.saturating_sub(n320 * perfect_factor);
297                remaining_hits = remaining_hits.saturating_sub(n320);
298            }
299
300            if let Some(n300) = self.n300 {
301                delta = delta.saturating_sub(n300 * 50);
302                remaining_hits = remaining_hits.saturating_sub(n300);
303            }
304
305            if let Some(n200) = self.n200 {
306                delta = delta.saturating_sub(n200 * 30);
307                remaining_hits = remaining_hits.saturating_sub(n200);
308            }
309
310            if let Some(n100) = self.n100 {
311                delta = delta.saturating_sub(n100 * 10);
312                remaining_hits = remaining_hits.saturating_sub(n100);
313            }
314
315            if let Some(n50) = self.n50 {
316                // should `delta` be adjusted here? unsure
317                remaining_hits = remaining_hits.saturating_sub(n50);
318            }
319
320            let mut perfects = if let Some(n320) = self.n320 {
321                n320
322            } else {
323                let perfects = u32::min(delta / perfect_factor, remaining_hits);
324                delta = delta.saturating_sub(perfects * perfect_factor);
325                remaining_hits = remaining_hits.saturating_sub(perfects);
326
327                perfects
328            };
329
330            let mut greats = if let Some(n300) = self.n300 {
331                n300
332            } else {
333                let greats = u32::min(delta / 50, remaining_hits);
334                delta = delta.saturating_sub(greats * 50);
335                remaining_hits = remaining_hits.saturating_sub(greats);
336
337                greats
338            };
339
340            let mut goods = if let Some(n200) = self.n200 {
341                n200
342            } else {
343                let goods = u32::min(delta / 30, remaining_hits);
344                delta = delta.saturating_sub(goods * 30);
345                remaining_hits = remaining_hits.saturating_sub(goods);
346
347                goods
348            };
349
350            let mut oks = if let Some(n100) = self.n100 {
351                n100
352            } else {
353                let oks = u32::min(delta / 10, remaining_hits);
354                remaining_hits = remaining_hits.saturating_sub(oks);
355
356                oks
357            };
358
359            let mehs = if let Some(mut n50) = self.n50 {
360                if remaining_hits > 0 {
361                    if self.n100.is_none() {
362                        oks += remaining_hits;
363                    } else if self.n200.is_none() {
364                        goods += remaining_hits;
365                    } else if self.n300.is_none() {
366                        greats += remaining_hits;
367                    } else if self.n320.is_none() {
368                        perfects += remaining_hits;
369                    } else {
370                        n50 += remaining_hits;
371                    }
372                }
373
374                n50
375            } else {
376                remaining_hits
377            };
378
379            ManiaScoreState {
380                n320: perfects,
381                n300: greats,
382                n200: goods,
383                n100: oks,
384                n50: mehs,
385                misses,
386            }
387        };
388
389        let generate_slow = |acc: f64| {
390            let target = acc * f64::from(if classic { 60 } else { 61 } * n_objects);
391
392            let mut best = ManiaScoreState {
393                n320,
394                n300,
395                n200,
396                n100,
397                n50: n_remaining.saturating_sub(n320 + n300 + n200 + n100),
398                misses,
399            };
400
401            let mut best_dist = f64::INFINITY;
402
403            let remaining = n_remaining.saturating_sub(n300 + n200 + n100 + n50);
404
405            let mut min_n320 = cmp::min(
406                (if classic {
407                    ((target - f64::from(40 * n_remaining) + f64::from(20 * n100 + 30 * n50))
408                        / 20.0)
409                        - f64::from(n300)
410                } else {
411                    target - f64::from(60 * n_remaining)
412                        + f64::from(20 * n200 + 40 * n100 + 50 * n50)
413                })
414                .floor() as u32,
415                remaining,
416            );
417
418            let mut max_n320 = cmp::min(
419                ((target - f64::from(10 * n_remaining + 50 * n300 + 30 * n200 + 10 * n100))
420                    / if classic { 50.0 } else { 51.0 })
421                .ceil() as u32,
422                remaining,
423            );
424
425            if let Some(n320) = self.n320 {
426                min_n320 = min_remaining(n320);
427                max_n320 = min_remaining(n320);
428            }
429
430            for n320 in min_n320..=max_n320 {
431                let remaining = n_remaining.saturating_sub(n320 + n200 + n100 + n50);
432
433                let mut min_n300 = cmp::min(
434                    (if classic && self.n320.is_none() {
435                        // n320 and n300 have the same value so we
436                        // generate them all via n320 and shift them
437                        // afterwards if necessary
438                        0.0
439                    } else {
440                        let n320_weight = if classic { 20 } else { 21 };
441
442                        (target - f64::from(40 * n_remaining + n320_weight * n320)
443                            + f64::from(20 * n100 + 30 * n50))
444                            / 20.0
445                    })
446                    .floor() as u32,
447                    remaining,
448                );
449
450                let mut max_n300 = cmp::min(
451                    (if classic && self.n320.is_none() {
452                        0.0
453                    } else {
454                        let n320_weight = if classic { 50 } else { 51 };
455
456                        (target
457                            - f64::from(
458                                10 * n_remaining + n320_weight * n320 + 30 * n200 + 10 * n100,
459                            ))
460                            / 50.0
461                    })
462                    .ceil() as u32,
463                    remaining,
464                );
465
466                if let Some(n300) = self.n300 {
467                    min_n300 = min_remaining(n300);
468                    max_n300 = min_remaining(n300);
469                }
470
471                for n300 in min_n300..=max_n300 {
472                    let remaining = n_remaining.saturating_sub(n320 + n300 + n100 + n50);
473
474                    let n320_weight = if classic { 50 } else { 51 };
475
476                    let mut min_n200 = cmp::min(
477                        ((target - f64::from(20 * n_remaining + n320_weight * n320 + 50 * n300)
478                            + f64::from(10 * n50))
479                            / 30.0)
480                            .floor() as u32,
481                        remaining,
482                    );
483
484                    let mut max_n200 = cmp::min(
485                        ((target
486                            - f64::from(
487                                10 * n_remaining + n320_weight * n320 + 50 * n300 + 10 * n100,
488                            ))
489                            / 30.0)
490                            .ceil() as u32,
491                        remaining,
492                    );
493
494                    if let Some(n200) = self.n200 {
495                        min_n200 = min_remaining(n200);
496                        max_n200 = min_remaining(n200);
497                    }
498
499                    for n200 in min_n200..=max_n200 {
500                        let n100s = if let Some(n100) = self.n100 {
501                            [min_remaining(n100), min_remaining(n100)]
502                        } else {
503                            let remaining = n_remaining.saturating_sub(n320 + n300 + n200 + n50);
504
505                            let n100_raw = if self.n50.is_some() {
506                                let n320_weight = if classic { 41 } else { 42 };
507
508                                target
509                                    - f64::from(
510                                        19 * n_remaining
511                                            + n320_weight * n320
512                                            + 41 * n300
513                                            + 21 * n200,
514                                    )
515                                    + f64::from(9 * n50)
516                            } else {
517                                let n320_weight = if classic { 50 } else { 51 };
518
519                                (target
520                                    - f64::from(
521                                        10 * n_remaining
522                                            + n320_weight * n320
523                                            + 50 * n300
524                                            + 30 * n200,
525                                    ))
526                                    / 10.0
527                            };
528
529                            let min = cmp::min(n100_raw.floor() as u32, remaining);
530                            let max = cmp::min(n100_raw.ceil() as u32, remaining);
531
532                            [min, max]
533                        };
534
535                        for n100 in n100s {
536                            let n50 = if let Some(n50) = self.n50 {
537                                min_remaining(n50)
538                            } else {
539                                n_remaining.saturating_sub(n320 + n300 + n200 + n100)
540                            };
541
542                            let mut curr = ManiaScoreState {
543                                n320,
544                                n300,
545                                n200,
546                                n100,
547                                n50,
548                                misses,
549                            };
550
551                            if curr.total_hits() < n_objects {
552                                let remaining = n_objects - curr.total_hits();
553
554                                match (self.n50, self.n100, self.n200, self.n300, self.n320) {
555                                    (None, ..) => curr.n50 += remaining,
556                                    (_, None, ..) => curr.n100 += remaining,
557                                    (_, _, None, ..) => curr.n200 += remaining,
558                                    (.., None, _) => curr.n300 += remaining,
559                                    (.., None) => curr.n320 += remaining,
560                                    _ => curr.n50 += remaining,
561                                }
562                            }
563
564                            let curr_acc = curr.accuracy(classic);
565                            let curr_dist = (acc - curr_acc).abs();
566
567                            if curr_dist < best_dist {
568                                best_dist = curr_dist;
569                                best = curr;
570                            }
571                        }
572                    }
573                }
574            }
575
576            // Only n320 have an increased effect on performance
577            // calculation so we adjust them based on priority
578            if classic && self.n320.is_none() {
579                // The logic below only operates on n320 and not n300
580                // so we shift them here and let the logic below do
581                // its thing
582                if self.n300.is_none() {
583                    best.n320 += best.n300;
584                    best.n300 = 0;
585                }
586
587                match priority {
588                    HitResultPriority::BestCase | HitResultPriority::Fastest => {
589                        if self.n100.is_none() && self.n200.is_none() {
590                            let n = best.n200 / 2;
591                            best.n320 += n;
592                            best.n200 -= 2 * n;
593                            best.n100 += n;
594                        }
595
596                        if self.n50.is_none() && self.n200.is_none() {
597                            let n = best.n200 / 5;
598                            best.n320 += n * 3;
599                            best.n200 -= n * 5;
600                            best.n50 += n * 2;
601                        }
602
603                        if self.n300.is_none() {
604                            best.n320 += best.n300;
605                            best.n300 = 0;
606                        }
607                    }
608                    HitResultPriority::WorstCase => {
609                        if self.n100.is_none() && self.n200.is_none() {
610                            let n = cmp::min(best.n320, best.n100);
611                            best.n320 -= n;
612                            best.n200 += 2 * n;
613                            best.n100 -= n;
614                        }
615
616                        if self.n50.is_none() && self.n200.is_none() {
617                            let n = cmp::min(best.n320 / 3, best.n50 / 2);
618                            best.n320 -= n * 3;
619                            best.n200 += n * 5;
620                            best.n50 -= n * 2;
621                        }
622
623                        if self.n300.is_none() {
624                            best.n300 += best.n320;
625                            best.n320 = 0;
626                        }
627                    }
628                }
629            }
630
631            best
632        };
633
634        if let Some(acc) = self.acc {
635            match (self.n320, self.n300, self.n200, self.n100, self.n50) {
636                // All hitresults given
637                (Some(_), Some(_), Some(_), Some(_), Some(_)) => {
638                    let remaining =
639                        n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + misses);
640
641                    match priority {
642                        HitResultPriority::BestCase | HitResultPriority::Fastest => {
643                            n320 += remaining;
644                        }
645                        HitResultPriority::WorstCase => n50 += remaining,
646                    }
647                }
648
649                // All but one hitresults given
650                (None, Some(_), Some(_), Some(_), Some(_)) => n320 = n_remaining,
651                (Some(_), None, Some(_), Some(_), Some(_)) => n300 = n_remaining,
652                (Some(_), Some(_), None, Some(_), Some(_)) => n200 = n_remaining,
653                (Some(_), Some(_), Some(_), None, Some(_)) => n100 = n_remaining,
654                (Some(_), Some(_), Some(_), Some(_), None) => n50 = n_remaining,
655
656                // At least two hitresults are unknown
657                _ => {
658                    let best = match priority {
659                        HitResultPriority::Fastest => generate_fast(acc),
660                        _ => generate_slow(acc),
661                    };
662
663                    n320 = best.n320;
664                    n300 = best.n300;
665                    n200 = best.n200;
666                    n100 = best.n100;
667                    n50 = best.n50;
668                }
669            }
670        } else {
671            let remaining = n_remaining.saturating_sub(n320 + n300 + n200 + n100 + n50);
672
673            match priority {
674                HitResultPriority::BestCase | HitResultPriority::Fastest => {
675                    match (self.n320, self.n300, self.n200, self.n100, self.n50) {
676                        (None, ..) => n320 = remaining,
677                        (_, None, ..) => n300 = remaining,
678                        (_, _, None, ..) => n200 = remaining,
679                        (.., None, _) => n100 = remaining,
680                        (.., None) => n50 = remaining,
681                        _ => n320 += remaining,
682                    }
683                }
684                HitResultPriority::WorstCase => {
685                    match (self.n50, self.n100, self.n200, self.n300, self.n320) {
686                        (None, ..) => n50 = remaining,
687                        (_, None, ..) => n100 = remaining,
688                        (_, _, None, ..) => n200 = remaining,
689                        (.., None, _) => n300 = remaining,
690                        (.., None) => n320 = remaining,
691                        _ => n50 += remaining,
692                    }
693                }
694            }
695        }
696
697        self.n320 = Some(n320);
698        self.n300 = Some(n300);
699        self.n200 = Some(n200);
700        self.n100 = Some(n100);
701        self.n50 = Some(n50);
702        self.misses = Some(misses);
703
704        Ok(ManiaScoreState {
705            n320,
706            n300,
707            n200,
708            n100,
709            n50,
710            misses,
711        })
712    }
713
714    /// Calculate all performance related values, including pp and stars.
715    pub fn calculate(mut self) -> Result<ManiaPerformanceAttributes, ConvertError> {
716        let state = self.generate_state()?;
717
718        let attrs = match self.map_or_attrs {
719            MapOrAttrs::Attrs(attrs) => attrs,
720            MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Mania>(map)?,
721        };
722
723        Ok(ManiaPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
724    }
725
726    pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Mania>) -> Self {
727        Self {
728            map_or_attrs,
729            difficulty: Difficulty::new(),
730            n320: None,
731            n300: None,
732            n200: None,
733            n100: None,
734            n50: None,
735            misses: None,
736            acc: None,
737            hitresult_priority: HitResultPriority::DEFAULT,
738        }
739    }
740}
741
742impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
743    type Error = OsuPerformance<'map>;
744
745    /// Try to create [`ManiaPerformance`] through [`OsuPerformance`].
746    ///
747    /// Returns `None` if [`OsuPerformance`] does not contain a beatmap, i.e.
748    /// if it was constructed through attributes or
749    /// [`OsuPerformance::generate_state`] was called.
750    fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
751        let mods = osu.difficulty.get_mods();
752
753        let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Mania, mods) {
754            Ok(map) => map,
755            Err(map_or_attrs) => {
756                osu.map_or_attrs = map_or_attrs;
757
758                return Err(osu);
759            }
760        };
761
762        let OsuPerformance {
763            map_or_attrs: _,
764            difficulty,
765            acc,
766            combo: _,
767            large_tick_hits: _,
768            small_tick_hits: _,
769            slider_end_hits: _,
770            n300,
771            n100,
772            n50,
773            misses,
774            hitresult_priority,
775        } = osu;
776
777        Ok(Self {
778            map_or_attrs: MapOrAttrs::Map(map),
779            difficulty,
780            n320: None,
781            n300,
782            n200: None,
783            n100,
784            n50,
785            misses,
786            acc,
787            hitresult_priority,
788        })
789    }
790}
791
792impl<'map, T: IntoModePerformance<'map, Mania>> From<T> for ManiaPerformance<'map> {
793    fn from(into: T) -> Self {
794        into.into_performance()
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use std::{cmp::Ordering, sync::OnceLock, time::Instant};
801
802    use proptest::{
803        prelude::*,
804        test_runner::{RngAlgorithm, TestRng},
805    };
806    use rosu_map::section::general::GameMode;
807    use rosu_mods::GameMod;
808
809    use crate::{
810        any::{DifficultyAttributes, PerformanceAttributes},
811        mania::ManiaDifficultyAttributes,
812        osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
813        Beatmap,
814    };
815
816    use super::{calculator::custom_accuracy, *};
817
818    static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new();
819
820    const N_OBJECTS: u32 = 594;
821    const N_HOLD_NOTES: u32 = 121;
822
823    fn beatmap() -> Beatmap {
824        Beatmap::from_path("./resources/1638954.osu").unwrap()
825    }
826
827    fn attrs() -> ManiaDifficultyAttributes {
828        ATTRS
829            .get_or_init(|| {
830                let map = beatmap();
831                let attrs = Difficulty::new().calculate_for_mode::<Mania>(&map).unwrap();
832
833                assert_eq!(N_OBJECTS, map.hit_objects.len() as u32);
834                assert_eq!(
835                    N_HOLD_NOTES,
836                    map.hit_objects.iter().filter(|h| !h.is_circle()).count() as u32
837                );
838
839                attrs
840            })
841            .to_owned()
842    }
843
844    /// Creates a [`rosu_mods::GameMods`] instance and inserts `CL` if `classic`
845    /// is true.
846    fn mods(classic: bool) -> rosu_mods::GameMods {
847        if classic {
848            let mut mods = rosu_mods::GameMods::new();
849            mods.insert(GameMod::ClassicMania(Default::default()));
850
851            mods
852        } else {
853            rosu_mods::GameMods::new()
854        }
855    }
856
857    /// Checks most remaining hitresult combinations w.r.t. the given parameters
858    /// and returns the [`ManiaScoreState`] that matches `acc` the best.
859    ///
860    /// Very slow but accurate. Only slight optimizations have been applied so
861    /// that it doesn't run unreasonably long.
862    #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
863    fn brute_force_best(
864        classic: bool,
865        acc: f64,
866        n320: Option<u32>,
867        n300: Option<u32>,
868        n200: Option<u32>,
869        n100: Option<u32>,
870        n50: Option<u32>,
871        misses: u32,
872        best_case: bool,
873    ) -> ManiaScoreState {
874        let misses = cmp::min(misses, N_OBJECTS);
875
876        let mut best_state = ManiaScoreState {
877            misses,
878            ..Default::default()
879        };
880
881        let mut best_dist = f64::INFINITY;
882        let mut best_custom_acc = 0.0;
883
884        let multiple_given = (usize::from(n320.is_some())
885            + usize::from(n300.is_some())
886            + usize::from(n200.is_some())
887            + usize::from(n100.is_some())
888            + usize::from(n50.is_some()))
889            > 1;
890
891        let mut n_objects = N_OBJECTS;
892
893        if !classic {
894            n_objects += N_HOLD_NOTES;
895        }
896
897        let n_remaining = n_objects - misses;
898
899        let target = acc * f64::from(if classic { 60 } else { 61 } * n_objects);
900
901        let max_left = n_objects.saturating_sub(
902            if classic { 0 } else { n300.unwrap_or(0) }
903                + n200.unwrap_or(0)
904                + n100.unwrap_or(0)
905                + n50.unwrap_or(0)
906                + misses,
907        );
908
909        let min_n320 = cmp::min(
910            max_left,
911            if classic {
912                (target - f64::from(40 * n_remaining)) / 20.0
913            } else {
914                target - f64::from(60 * n_remaining)
915            }
916            .floor() as u32,
917        );
918
919        let max_n320 = cmp::min(
920            max_left,
921            ((target - f64::from(10 * n_remaining)) / if classic { 50.0 } else { 51.0 }).ceil()
922                as u32,
923        );
924
925        let (min_n320, max_n320) = match (n320, n300) {
926            (Some(n320), _) if !classic => {
927                (cmp::min(n_remaining, n320), cmp::min(n_remaining, n320))
928            }
929            (None, _) if !classic => (min_n320, max_n320),
930            (Some(n320), Some(n300)) => (
931                cmp::min(n_remaining, n320 + n300),
932                cmp::min(n_remaining, n320 + n300),
933            ),
934            (Some(n320), None) => (
935                cmp::max(cmp::min(n_remaining, n320), min_n320),
936                cmp::max(max_n320, cmp::min(n320, n_remaining)),
937            ),
938            (None, Some(n300)) => (
939                cmp::max(cmp::min(n_remaining, n300), min_n320),
940                cmp::max(max_n320, cmp::min(n300, n_remaining)),
941            ),
942            (None, None) => (min_n320, max_n320),
943        };
944
945        let mut n300_iters = 0;
946        let mut n300_skips = 0;
947
948        for new320 in min_n320..=max_n320 {
949            let max_left = n_remaining
950                .saturating_sub(new320 + n200.unwrap_or(0) + n100.unwrap_or(0) + n50.unwrap_or(0));
951
952            let (min_n300, max_n300) = match n300 {
953                _ if classic => (0, 0),
954                Some(n300) if multiple_given => {
955                    (cmp::min(n_remaining, n300), cmp::min(n_remaining, n300))
956                }
957                Some(n300) => (cmp::min(max_left, n300), cmp::min(max_left, n300)),
958                None if n200.and(n100).and(n50).is_some() => (max_left, max_left),
959                None => (0, max_left),
960            };
961
962            for new300 in min_n300..=max_n300 {
963                let max_left = n_remaining
964                    .saturating_sub(new320 + new300 + n100.unwrap_or(0) + n50.unwrap_or(0));
965
966                let min_state = {
967                    let n50 = n50.unwrap_or(max_left);
968                    let n100 = n100.unwrap_or(
969                        n_remaining.saturating_sub(new320 + new300 + n200.unwrap_or(0) + n50),
970                    );
971                    let n200 =
972                        n200.unwrap_or(n_remaining.saturating_sub(new320 + new300 + n100 + n50));
973
974                    ManiaScoreState {
975                        n320: new320,
976                        n300: new300,
977                        n200,
978                        n100,
979                        n50,
980                        misses,
981                    }
982                };
983
984                let max_state = {
985                    let n200 = n200.unwrap_or(max_left);
986                    let n100 = n100.unwrap_or(
987                        n_remaining.saturating_sub(new320 + new300 + n200 + n50.unwrap_or(0)),
988                    );
989                    let n50 =
990                        n50.unwrap_or(n_remaining.saturating_sub(new320 + new300 + n200 + n100));
991
992                    ManiaScoreState {
993                        n320: new320,
994                        n300: new300,
995                        n200,
996                        n100,
997                        n50,
998                        misses,
999                    }
1000                };
1001
1002                n300_iters += 1;
1003
1004                // Skip n200 and n100 iterations if we know we won't be able to
1005                // get a better result.
1006                if min_state.accuracy(classic) - best_dist > acc
1007                    || max_state.accuracy(classic) + best_dist < acc
1008                {
1009                    n300_skips += 1;
1010
1011                    continue;
1012                }
1013
1014                let (min_n200, max_n200) = match (n200, n100, n50) {
1015                    (Some(n200), ..) if multiple_given => {
1016                        (cmp::min(n_remaining, n200), cmp::min(n_remaining, n200))
1017                    }
1018                    (Some(n200), ..) => (cmp::min(max_left, n200), cmp::min(max_left, n200)),
1019                    (None, Some(_), Some(_)) => (max_left, max_left),
1020                    _ => (0, max_left),
1021                };
1022
1023                for new200 in min_n200..=max_n200 {
1024                    let max_left =
1025                        n_remaining.saturating_sub(new320 + new300 + new200 + n50.unwrap_or(0));
1026
1027                    let (min_n100, max_n100) = match (n100, n50) {
1028                        (Some(n100), _) if multiple_given => {
1029                            (cmp::min(n_remaining, n100), cmp::min(n_remaining, n100))
1030                        }
1031                        (Some(n100), _) => (cmp::min(max_left, n100), cmp::min(max_left, n100)),
1032                        (None, Some(_)) => (max_left, max_left),
1033                        (None, None) => (0, max_left),
1034                    };
1035
1036                    for new100 in min_n100..=max_n100 {
1037                        let max_left =
1038                            n_remaining.saturating_sub(new320 + new300 + new200 + new100);
1039
1040                        let new50 = match n50 {
1041                            Some(n50) if multiple_given => cmp::min(n_remaining, n50),
1042                            Some(n50) => cmp::min(max_left, n50),
1043                            None => max_left,
1044                        };
1045
1046                        let (new320, new300) = if classic {
1047                            match (n320, n300) {
1048                                (Some(n320), Some(n300)) => {
1049                                    (cmp::min(n_remaining, n320), cmp::min(n_remaining, n300))
1050                                }
1051                                (Some(n320), None) => (
1052                                    cmp::min(n320, n_remaining),
1053                                    new320 - cmp::min(n320, n_remaining),
1054                                ),
1055                                (None, Some(n300)) => (
1056                                    new320 - cmp::min(n300, n_remaining),
1057                                    cmp::min(n300, n_remaining),
1058                                ),
1059                                (None, None) if best_case => (new320, 0),
1060                                (None, None) => (0, new320),
1061                            }
1062                        } else {
1063                            (new320, new300)
1064                        };
1065
1066                        let curr_acc = ManiaScoreState {
1067                            n320: new320,
1068                            n300: new300,
1069                            n200: new200,
1070                            n100: new100,
1071                            n50: new50,
1072                            misses,
1073                        }
1074                        .accuracy(classic);
1075
1076                        let curr_dist = (acc - curr_acc).abs();
1077
1078                        let curr_custom_acc =
1079                            custom_accuracy(new320, new300, new200, new100, new50, n_objects);
1080
1081                        match curr_dist.total_cmp(&best_dist) {
1082                            Ordering::Less => {
1083                                best_dist = curr_dist;
1084                                best_custom_acc = curr_custom_acc;
1085                                best_state.n320 = new320;
1086                                best_state.n300 = new300;
1087                                best_state.n200 = new200;
1088                                best_state.n100 = new100;
1089                                best_state.n50 = new50;
1090                            }
1091                            Ordering::Equal if curr_custom_acc < best_custom_acc => {
1092                                best_custom_acc = curr_custom_acc;
1093                                best_state.n320 = new320;
1094                                best_state.n300 = new300;
1095                                best_state.n200 = new200;
1096                                best_state.n100 = new100;
1097                                best_state.n50 = new50;
1098                            }
1099                            _ => {}
1100                        }
1101                    }
1102                }
1103            }
1104        }
1105
1106        eprintln!("Bruteforce skipped {n300_skips}/{n300_iters} n300 iterations");
1107
1108        if best_state.n320 + best_state.n300 + best_state.n200 + best_state.n100 + best_state.n50
1109            < n_remaining
1110        {
1111            let n_remaining = n_remaining
1112                - (best_state.n320
1113                    + best_state.n300
1114                    + best_state.n200
1115                    + best_state.n100
1116                    + best_state.n50);
1117
1118            if best_case {
1119                match (n320, n300, n200, n100, n50) {
1120                    (None, ..) => best_state.n320 += n_remaining,
1121                    (_, None, ..) => best_state.n300 += n_remaining,
1122                    (_, _, None, ..) => best_state.n200 += n_remaining,
1123                    (.., None, _) => best_state.n100 += n_remaining,
1124                    (.., None) => best_state.n50 += n_remaining,
1125                    _ => best_state.n320 += n_remaining,
1126                }
1127            } else {
1128                match (n50, n100, n200, n300, n320) {
1129                    (None, ..) => best_state.n50 += n_remaining,
1130                    (_, None, ..) => best_state.n100 += n_remaining,
1131                    (_, _, None, ..) => best_state.n200 += n_remaining,
1132                    (.., None, _) => best_state.n300 += n_remaining,
1133                    (.., None) => best_state.n320 += n_remaining,
1134                    _ => best_state.n50 += n_remaining,
1135                }
1136            }
1137        }
1138
1139        if classic && n320.is_none() {
1140            let before = best_state.clone();
1141
1142            if n300.is_none() {
1143                best_state.n320 += best_state.n300;
1144                best_state.n300 = 0;
1145            }
1146
1147            if best_case {
1148                if n100.is_none() && n200.is_none() {
1149                    let n = best_state.n200 / 2;
1150                    best_state.n320 += n;
1151                    best_state.n200 -= 2 * n;
1152                    best_state.n100 += n;
1153                }
1154
1155                if n50.is_none() && n200.is_none() {
1156                    let n = best_state.n200 / 5;
1157                    best_state.n320 += n * 3;
1158                    best_state.n200 -= n * 5;
1159                    best_state.n50 += n * 2;
1160                }
1161
1162                if n300.is_none() {
1163                    best_state.n320 += best_state.n300;
1164                    best_state.n300 = 0;
1165                }
1166            } else {
1167                if n100.is_none() && n200.is_none() {
1168                    let n = cmp::min(best_state.n320, best_state.n100);
1169                    best_state.n320 -= n;
1170                    best_state.n200 += 2 * n;
1171                    best_state.n100 -= n;
1172                }
1173
1174                if n50.is_none() && n200.is_none() {
1175                    let n = cmp::min(best_state.n320 / 3, best_state.n50 / 2);
1176                    best_state.n320 -= n * 3;
1177                    best_state.n200 += n * 5;
1178                    best_state.n50 -= n * 2;
1179                }
1180
1181                if n300.is_none() {
1182                    best_state.n300 += best_state.n320;
1183                    best_state.n320 = 0;
1184                }
1185            }
1186
1187            assert_eq!(best_state.accuracy(classic), before.accuracy(classic));
1188        }
1189
1190        best_state
1191    }
1192
1193    proptest! {
1194        #![proptest_config(ProptestConfig {
1195            cases: 20,
1196            ..Default::default()
1197        })]
1198
1199        #[test]
1200        #[ignore = "cannot skip persistent failure cases for some reason which run way too slowly"]
1201        fn mania_hitresults(
1202            classic in prop::bool::ANY,
1203            acc in 0.0_f64..=1.0,
1204            n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1205            n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1206            n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1207            n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1208            n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1209            n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1210            best_case in prop::bool::ANY,
1211        ) {
1212            exec_mania_hitresults(classic, acc, n320, n300, n200, n100, n50, n_misses, best_case);
1213        }
1214    }
1215
1216    #[test]
1217    fn rng_mania_hitresults() {
1218        /// Generates a random seed by measuring the time it takes to calculate
1219        /// all primes up to 10_000.
1220        fn generate_seed() -> [u8; 16] {
1221            let start = Instant::now();
1222
1223            const LIMIT: usize = 10_000;
1224            let mut is_prime = vec![true; LIMIT + 1];
1225            is_prime.iter_mut().step_by(2).for_each(|n| *n = false);
1226            is_prime[1] = false;
1227            is_prime[2] = true;
1228
1229            for n in (3..=LIMIT).step_by(2) {
1230                if !is_prime[n] {
1231                    continue;
1232                }
1233
1234                for m in (n * n..=LIMIT).step_by(n) {
1235                    is_prime[m] = false;
1236                }
1237            }
1238
1239            start.elapsed().as_nanos().to_le_bytes()
1240        }
1241
1242        let seed = generate_seed();
1243        eprintln!("seed={seed:?}");
1244        let mut rng = TestRng::from_seed(RngAlgorithm::XorShift, &seed);
1245
1246        // Worst-case test cases can take over 5 minutes to bruteforce on debug
1247        // mode so we shouldn't over do the amount here.
1248        const CASES: usize = 4;
1249
1250        for _ in 0..CASES {
1251            const LIMIT: u32 = N_OBJECTS + N_HOLD_NOTES + 10;
1252
1253            let classic = rng.gen();
1254            let acc = rng.gen_range(0.0..=1.0);
1255            let n320 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1256            let n300 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1257            let n200 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1258            let n100 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1259            let n50 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1260            let n_misses = rng.gen_bool(0.2).then(|| rng.gen_range(0..=LIMIT));
1261            let best_case = rng.gen();
1262
1263            eprintln!(
1264                "classic={} | acc={} | n320={:?} | n300={:?} | n200={:?} | \
1265                n100={:?} | n50={:?} | n_misses={:?} | best_case={}",
1266                classic, acc, n320, n300, n200, n100, n50, n_misses, best_case,
1267            );
1268
1269            exec_mania_hitresults(
1270                classic, acc, n320, n300, n200, n100, n50, n_misses, best_case,
1271            );
1272        }
1273    }
1274
1275    fn exec_mania_hitresults(
1276        classic: bool,
1277        acc: f64,
1278        n320: Option<u32>,
1279        n300: Option<u32>,
1280        n200: Option<u32>,
1281        n100: Option<u32>,
1282        n50: Option<u32>,
1283        n_misses: Option<u32>,
1284        best_case: bool,
1285    ) {
1286        let priority = if best_case {
1287            HitResultPriority::BestCase
1288        } else {
1289            HitResultPriority::WorstCase
1290        };
1291
1292        let mut state = ManiaPerformance::from(attrs())
1293            .accuracy(acc * 100.0)
1294            .lazer(!classic)
1295            .mods(mods(classic))
1296            .hitresult_priority(priority);
1297
1298        if let Some(n320) = n320 {
1299            state = state.n320(n320);
1300        }
1301
1302        if let Some(n300) = n300 {
1303            state = state.n300(n300);
1304        }
1305
1306        if let Some(n200) = n200 {
1307            state = state.n200(n200);
1308        }
1309
1310        if let Some(n100) = n100 {
1311            state = state.n100(n100);
1312        }
1313
1314        if let Some(n50) = n50 {
1315            state = state.n50(n50);
1316        }
1317
1318        if let Some(misses) = n_misses {
1319            state = state.misses(misses);
1320        }
1321
1322        let start = Instant::now();
1323        let first = state.generate_state().unwrap();
1324        let state_elapsed = start.elapsed();
1325        let state = state.generate_state().unwrap();
1326        assert_eq!(first, state);
1327
1328        let start = Instant::now();
1329        let expected = brute_force_best(
1330            classic,
1331            acc,
1332            n320,
1333            n300,
1334            n200,
1335            n100,
1336            n50,
1337            n_misses.unwrap_or(0),
1338            best_case,
1339        );
1340        let bf_elapsed = start.elapsed();
1341
1342        eprintln!("Elapsed: state={state_elapsed:?} bf={bf_elapsed:?}");
1343
1344        assert_eq!(
1345            state,
1346            expected,
1347            "dist: {} vs {}",
1348            (state.accuracy(classic) - acc).abs(),
1349            (expected.accuracy(classic) - acc).abs(),
1350        );
1351    }
1352
1353    #[test]
1354    fn hitresults_n320_misses_best() {
1355        let classic = true;
1356
1357        let state = ManiaPerformance::from(attrs())
1358            .lazer(!classic)
1359            .mods(mods(classic))
1360            .n320(500)
1361            .misses(2)
1362            .hitresult_priority(HitResultPriority::BestCase)
1363            .generate_state()
1364            .unwrap();
1365
1366        let expected = ManiaScoreState {
1367            n320: 500,
1368            n300: 92,
1369            n200: 0,
1370            n100: 0,
1371            n50: 0,
1372            misses: 2,
1373        };
1374
1375        assert_eq!(state, expected);
1376    }
1377
1378    #[test]
1379    fn hitresults_n100_n50_misses_worst() {
1380        let classic = true;
1381
1382        let state = ManiaPerformance::from(attrs())
1383            .lazer(!classic)
1384            .mods(mods(classic))
1385            .n100(200)
1386            .n50(50)
1387            .misses(2)
1388            .hitresult_priority(HitResultPriority::WorstCase)
1389            .generate_state()
1390            .unwrap();
1391
1392        let expected = ManiaScoreState {
1393            n320: 0,
1394            n300: 0,
1395            n200: 342,
1396            n100: 200,
1397            n50: 50,
1398            misses: 2,
1399        };
1400
1401        assert_eq!(state, expected);
1402    }
1403
1404    #[test]
1405    fn create() {
1406        let mut map = beatmap();
1407
1408        let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
1409        let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
1410        let _ = ManiaPerformance::new(&map);
1411        let _ = ManiaPerformance::new(map.clone());
1412
1413        let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
1414        let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
1415        let _ = ManiaPerformance::try_new(DifficultyAttributes::Mania(
1416            ManiaDifficultyAttributes::default(),
1417        ))
1418        .unwrap();
1419        let _ = ManiaPerformance::try_new(PerformanceAttributes::Mania(
1420            ManiaPerformanceAttributes::default(),
1421        ))
1422        .unwrap();
1423        let _ = ManiaPerformance::try_new(&map).unwrap();
1424        let _ = ManiaPerformance::try_new(map.clone()).unwrap();
1425
1426        let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
1427        let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
1428        let _ = ManiaPerformance::from(&map);
1429        let _ = ManiaPerformance::from(map.clone());
1430
1431        let _ = ManiaDifficultyAttributes::default().performance();
1432        let _ = ManiaPerformanceAttributes::default().performance();
1433
1434        assert!(map
1435            .convert_mut(GameMode::Osu, &GameMods::default())
1436            .is_err());
1437
1438        assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
1439        assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
1440        assert!(ManiaPerformance::try_new(DifficultyAttributes::Osu(
1441            OsuDifficultyAttributes::default()
1442        ))
1443        .is_none());
1444        assert!(ManiaPerformance::try_new(PerformanceAttributes::Osu(
1445            OsuPerformanceAttributes::default()
1446        ))
1447        .is_none());
1448    }
1449}