rosu-pp 4.0.1

Difficulty and performance calculation for osu!
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
use std::panic::{self, UnwindSafe};

use rosu_pp::{
    Beatmap, Difficulty,
    catch::{Catch, CatchDifficultyAttributes},
    mania::{Mania, ManiaDifficultyAttributes},
    osu::{Osu, OsuDifficultyAttributes},
    taiko::{Taiko, TaikoDifficultyAttributes},
};

use self::common::*;

mod common;

macro_rules! test_cases {
    ( $mode:ident: $path:ident {
        $( $( $mods:ident )+ => {
            $( $key:ident: $value:literal $( , )? )*
        } $( ; )? )*
    } ) => {
        let map = Beatmap::from_path(common::$path).unwrap();

        $(
            let mods = 0 $( + $mods )*;
            let expected = test_cases!(@$mode { $( $key: $value, )* });

            let actual = Difficulty::new()
                .mods(mods)
                .calculate_for_mode::<$mode>(&map)
                .unwrap();

            run(&actual, &expected, mods);
        )*
    };
    ( @Osu {
        aim: $aim:literal,
        aim_difficult_slider_count: $aim_difficult_slider_count:literal,
        speed: $speed:literal,
        flashlight: $flashlight:literal,
        slider_factor: $slider_factor:literal,
        aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor:literal,
        speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor:literal,
        speed_note_count: $speed_note_count:literal,
        aim_difficult_strain_count: $aim_difficult_strain_count:literal,
        speed_difficult_strain_count: $speed_difficult_strain_count:literal,
        nested_score_per_object: $nested_score_per_object:literal,
        legacy_score_base_multiplier: $legacy_score_base_multiplier:literal,
        maximum_legacy_combo_score: $maximum_legacy_combo_score:literal,
        ar: $ar:literal,
        great_hit_window: $great_hit_window:literal,
        ok_hit_window: $ok_hit_window:literal,
        meh_hit_window: $meh_hit_window:literal,
        hp: $hp:literal,
        n_circles: $n_circles:literal,
        n_sliders: $n_sliders:literal,
        n_large_ticks: $n_large_ticks:literal,
        n_spinners: $n_spinners:literal,
        stars: $stars:literal,
        max_combo: $max_combo:literal,
    }) => {
        OsuDifficultyAttributes {
            aim: $aim,
            aim_difficult_slider_count: $aim_difficult_slider_count,
            speed: $speed,
            flashlight: $flashlight,
            slider_factor: $slider_factor,
            aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor,
            speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor,
            speed_note_count: $speed_note_count,
            aim_difficult_strain_count: $aim_difficult_strain_count,
            speed_difficult_strain_count: $speed_difficult_strain_count,
            nested_score_per_object: $nested_score_per_object,
            legacy_score_base_multiplier: $legacy_score_base_multiplier,
            maximum_legacy_combo_score: $maximum_legacy_combo_score,
            ar: $ar,
            great_hit_window: $great_hit_window,
            ok_hit_window: $ok_hit_window,
            meh_hit_window: $meh_hit_window,
            hp: $hp,
            n_circles: $n_circles,
            n_sliders: $n_sliders,
            n_large_ticks: $n_large_ticks,
            n_spinners: $n_spinners,
            stars: $stars,
            max_combo: $max_combo,
        }
    };
    ( @Taiko {
        stamina: $stamina:literal,
        rhythm: $rhythm:literal,
        color: $color:literal,
        reading: $reading:literal,
        great_hit_window: $great_hit_window:literal,
        ok_hit_window: $ok_hit_window:literal,
        mono_stamina_factor: $mono_stamina_factor:literal,
        mechanical_difficulty: $mechanical_difficulty:literal,
        consistency_factor: $consistency_factor:literal,
        stars: $stars:literal,
        max_combo: $max_combo:literal,
        is_convert: $is_convert:literal,
    }) => {
        TaikoDifficultyAttributes {
            stamina: $stamina,
            rhythm: $rhythm,
            color: $color,
            reading: $reading,
            great_hit_window: $great_hit_window,
            ok_hit_window: $ok_hit_window,
            mono_stamina_factor: $mono_stamina_factor,
            mechanical_difficulty: $mechanical_difficulty,
            consistency_factor: $consistency_factor,
            stars: $stars,
            max_combo: $max_combo,
            is_convert: $is_convert,
        }
    };
    ( @Catch {
        stars: $stars:literal,
        preempt: $preempt:literal,
        n_fruits: $n_fruits:literal,
        n_droplets: $n_droplets:literal,
        n_tiny_droplets: $n_tiny_droplets:literal,
        is_convert: $is_convert:literal,
    }) => {
        CatchDifficultyAttributes {
            stars: $stars,
            preempt: $preempt,
            n_fruits: $n_fruits,
            n_droplets: $n_droplets,
            n_tiny_droplets: $n_tiny_droplets,
            is_convert: $is_convert,
        }
    };
    ( @Mania {
        stars: $stars:literal,
        n_objects: $n_objects:literal,
        n_hold_notes: $n_hold_notes:literal,
        max_combo: $max_combo:literal,
        is_convert: $is_convert:literal,
    }) => {
        ManiaDifficultyAttributes {
            stars: $stars,
            n_objects: $n_objects,
            n_hold_notes: $n_hold_notes,
            max_combo: $max_combo,
            is_convert: $is_convert,
        }
    }
}

#[test]
fn basic_osu() {
    #[cfg(target_os = "windows")]
    test_cases! {
        Osu: OSU {
            NM => {
                aim: 3.021506412510076,
                aim_difficult_slider_count: 180.33980678704012,
                speed: 2.5263145770639976,
                flashlight: 0.0,
                slider_factor: 0.9847225384137204,
                aim_top_weighted_slider_factor: 1.3996332540321264,
                speed_top_weighted_slider_factor: 0.6014562852677632,
                speed_note_count: 202.24319351543616,
                aim_difficult_strain_count: 108.47555309841259,
                speed_difficult_strain_count: 78.39830024782772,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 9.300000190734863,
                great_hit_window: 26.5,
                ok_hit_window: 68.5,
                meh_hit_window: 110.5,
                hp: 5.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 5.740766046562339,
                max_combo: 909,
            };
            HD => {
                aim: 3.121489829231887,
                aim_difficult_slider_count: 180.33980678704012,
                speed: 2.614171127905441,
                flashlight: 0.0,
                slider_factor: 0.9847225384137204,
                aim_top_weighted_slider_factor: 1.3996332540321264,
                speed_top_weighted_slider_factor: 0.6014562852677632,
                speed_note_count: 202.24319351543616,
                aim_difficult_strain_count: 108.47555309841259,
                speed_difficult_strain_count: 78.39830024782772,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 9.300000190734863,
                great_hit_window: 26.5,
                ok_hit_window: 68.5,
                meh_hit_window: 110.5,
                hp: 5.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 5.934133851244851,
                max_combo: 909,
            };
            HR => {
                aim: 3.4309052630747257,
                aim_difficult_slider_count: 187.20300643263465,
                speed: 2.6813963801152716,
                flashlight: 0.0,
                slider_factor: 0.9748562752795166,
                aim_top_weighted_slider_factor: 1.3634873114118244,
                speed_top_weighted_slider_factor: 0.6668815233244475,
                speed_note_count: 185.01178339020348,
                aim_difficult_strain_count: 112.28112750203013,
                speed_difficult_strain_count: 74.53251006179151,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 10.0,
                great_hit_window: 19.5,
                ok_hit_window: 59.5,
                meh_hit_window: 99.5,
                hp: 7.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 6.375104448752039,
                max_combo: 909,
            };
            DT => {
                aim: 4.3662195513104525,
                aim_difficult_slider_count: 195.41476682131653,
                speed:  3.7793477426295814,
                flashlight: 0.0,
                slider_factor: 0.9787310737204966,
                aim_top_weighted_slider_factor: 1.3819099517666353,
                speed_top_weighted_slider_factor: 0.6923456235877925,
                speed_note_count: 208.98215163620375,
                aim_difficult_strain_count: 130.48279566301667,
                speed_difficult_strain_count: 93.64469563382437,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 10.533333460489908,
                great_hit_window: 17.666666666666668,
                ok_hit_window: 45.666666666666664,
                meh_hit_window: 73.66666666666667,
                hp: 5.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 8.40182116136074,
                max_combo: 909,
            };
            FL => {
                aim: 3.021506412510076,
                aim_difficult_slider_count: 180.33980678704012,
                speed: 2.5263145770639976,
                flashlight: 2.3005989208967885,
                slider_factor: 0.9847225384137204,
                aim_top_weighted_slider_factor: 1.3996332540321264,
                speed_top_weighted_slider_factor: 0.6014562852677632,
                speed_note_count: 202.24319351543616,
                aim_difficult_strain_count: 108.47555309841259,
                speed_difficult_strain_count: 78.39830024782772,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 9.300000190734863,
                great_hit_window: 26.5,
                ok_hit_window: 68.5,
                meh_hit_window: 110.5,
                hp: 5.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 6.864308231959398,
                max_combo: 909,
            };
            HD EZ => {
                aim: 2.9818506123002706,
                aim_difficult_slider_count: 173.00759261125233,
                speed: 2.511531850339625,
                flashlight: 0.0,
                slider_factor: 0.9931728395338801,
                aim_top_weighted_slider_factor: 1.4758126955064637,
                speed_top_weighted_slider_factor: 0.48777057881852615,
                speed_note_count: 211.97339651166865,
                aim_difficult_strain_count: 107.45480335487801,
                speed_difficult_strain_count: 78.94432491731223,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 3.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 4.650000095367432,
                great_hit_window: 52.5,
                ok_hit_window: 103.5,
                meh_hit_window: 154.5,
                hp: 2.5,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 5.680319048111094,
                max_combo: 909,
            };
            HD FL => {
                aim: 3.121489829231887,
                aim_difficult_slider_count: 180.33980678704012,
                speed: 2.614171127905441,
                flashlight: 2.620335643475851,
                slider_factor: 0.9847225384137204,
                aim_top_weighted_slider_factor: 1.3996332540321264,
                speed_top_weighted_slider_factor: 0.6014562852677632,
                speed_note_count: 202.24319351543616,
                aim_difficult_strain_count: 108.47555309841259,
                speed_difficult_strain_count: 78.39830024782772,
                nested_score_per_object: 34.991680532445926,
                legacy_score_base_multiplier: 5.0,
                maximum_legacy_combo_score: 15729840.0,
                ar: 9.300000190734863,
                great_hit_window: 26.5,
                ok_hit_window: 68.5,
                meh_hit_window: 110.5,
                hp: 5.0,
                n_circles: 307,
                n_sliders: 293,
                n_large_ticks: 15,
                n_spinners: 1,
                stars: 7.2736222258399374,
                max_combo: 909,
            };
        }
    };
}

#[test]
fn basic_taiko() {
    #[cfg(target_os = "windows")]
    test_cases! {
        Taiko: TAIKO {
            NM => {
                stamina: 2.0538739969959194,
                rhythm: 0.20910773140367978,
                color: 0.6533063635604147,
                reading: 1.6783022238696096E-05,
                great_hit_window: 34.5,
                ok_hit_window: 79.5,
                mono_stamina_factor: 2.585220903145618E-07,
                mechanical_difficulty: 2.7071803605563343,
                consistency_factor: 0.6315890845261888,
                stars: 2.9163048749822527,
                max_combo: 289,
                is_convert: false,
            };
            HR => {
                stamina: 1.7575509868953703,
                rhythm: 0.1803685258404691,
                color: 0.5590504800684293,
                reading: 0.5006234843367281,
                great_hit_window: 28.5,
                ok_hit_window: 67.5,
                mono_stamina_factor: 2.585220903145618E-07,
                mechanical_difficulty: 2.3166014669637995,
                consistency_factor: 0.6323578625670226,
                stars: 2.9975934771409967,
                max_combo: 289,
                is_convert: false,
            };
            DT => {
                stamina: 2.5520061698054346,
                rhythm: 0.5994985831319557,
                color: 0.7173945446143202,
                reading: 0.18956996617034277,
                great_hit_window: 23.0,
                ok_hit_window: 53.0,
                mono_stamina_factor: 2.465693827167051E-07,
                mechanical_difficulty: 3.2694007144197546,
                consistency_factor: 0.621885489483516,
                stars: 4.058469263722054,
                max_combo: 289,
                is_convert: false,
            };
        }
    };
}

#[test]
fn convert_taiko() {
    test_cases! {
        Taiko: OSU {
            NM => {
                stamina: 2.207066240409116,
                rhythm: 0.8365139147559882,
                color: 0.8396618490452487,
                reading: 1.0735173315689464,
                great_hit_window: 22.5,
                ok_hit_window: 56.5,
                mono_stamina_factor: 0.0014311041774359666,
                mechanical_difficulty: 3.0467280894543647,
                consistency_factor: 0.6655548318078143,
                stars: 4.9567593357792985,
                max_combo: 908,
                is_convert: true,
            };
            HR => {
                stamina: 2.2682938123290377,
                rhythm: 0.8719256263669988,
                color: 0.8629554209868424,
                reading: 1.4197363916621928,
                great_hit_window: 19.5,
                ok_hit_window: 49.5,
                mono_stamina_factor: 0.0014311041774359666,
                mechanical_difficulty: 3.13124923331588,
                consistency_factor: 0.6679579465054277,
                stars: 5.422911251345071,
                max_combo: 908,
                is_convert: true,
            };
            DT => {
                stamina: 3.210799549552373,
                rhythm: 1.2585331678081546,
                color: 1.0837796585509605,
                reading: 1.7833409886818568,
                great_hit_window: 15.0,
                ok_hit_window: 37.666666666666664,
                mono_stamina_factor: 0.0014418086037955797,
                mechanical_difficulty: 4.294579208103333,
                consistency_factor: 0.6621583643602745,
                stars: 7.336453364593345,
                max_combo: 908,
                is_convert: true,
            };
        }
    };
}

#[test]
fn basic_catch() {
    test_cases! {
        Catch: CATCH {
            NM => {
                stars: 3.250266313373984,
                preempt: 750.0,
                n_fruits: 728,
                n_droplets: 2,
                n_tiny_droplets: 263,
                is_convert: false,
            };
            HR => {
                stars: 4.313360856186517,
                preempt: 450.0,
                n_fruits: 728,
                n_droplets: 2,
                n_tiny_droplets: 263,
                is_convert: false,
            };
            EZ => {
                stars: 4.06522224010957,
                preempt: 1320.0,
                n_fruits: 728,
                n_droplets: 2,
                n_tiny_droplets: 263,
                is_convert: false,
            };
            DT => {
                stars: 4.635262826575386,
                preempt: 500.0,
                n_fruits: 728,
                n_droplets: 2,
                n_tiny_droplets: 263,
                is_convert: false,
            };
        }
    };
}

#[test]
fn convert_catch() {
    test_cases! {
        Catch: OSU {
            NM => {
                stars: 4.528720977989276,
                preempt: 554.9999713897705,
                n_fruits: 908,
                n_droplets: 0,
                n_tiny_droplets: 159,
                is_convert: true,
            };
            HR => {
                stars: 5.076698043567007,
                preempt: 450.0,
                n_fruits: 908,
                n_droplets: 0,
                n_tiny_droplets: 159,
                is_convert: true,
            };
            EZ => {
                stars: 3.593264064535228,
                preempt: 1241.9999885559082,
                n_fruits: 908,
                n_droplets: 0,
                n_tiny_droplets: 159,
                is_convert: true,
            };
            DT => {
                stars: 6.15540143757313,
                preempt: 369.9999809265137,
                n_fruits: 908,
                n_droplets: 0,
                n_tiny_droplets: 159,
                is_convert: true,
            };
        }
    };
}

#[test]
fn basic_mania() {
    test_cases! {
        Mania: MANIA {
            NM => {
                stars: 3.358304846842773,
                n_objects: 594,
                n_hold_notes: 121,
                max_combo: 956,
                is_convert: false,
            };
            DT => {
                stars: 4.6072892053157295,
                n_objects: 594,
                n_hold_notes: 121,
                max_combo: 956,
                is_convert: false,
            };
        }
    };
}

#[test]
fn convert_mania() {
    test_cases! {
        Mania: OSU {
            NM => {
                stars: 3.2033142085672255,
                n_objects: 1046,
                n_hold_notes: 266,
                max_combo: 1381,
                is_convert: true,
            };
            DT => {
                stars: 4.2934063021960185,
                n_objects: 1046,
                n_hold_notes: 266,
                max_combo: 1381,
                is_convert: true,
            };
        }
    };
}

fn run<A>(actual: &A, expected: &A, mods: u32)
where
    A: AssertEq,
    for<'a> &'a A: UnwindSafe,
{
    if panic::catch_unwind(|| actual.assert_eq(expected)).is_err() {
        panic!("Mods: {mods}");
    }
}

impl AssertEq for OsuDifficultyAttributes {
    fn assert_eq(&self, expected: &Self) {
        let Self {
            aim,
            aim_difficult_slider_count,
            speed,
            flashlight,
            slider_factor,
            aim_top_weighted_slider_factor,
            speed_top_weighted_slider_factor,
            speed_note_count,
            aim_difficult_strain_count,
            speed_difficult_strain_count,
            nested_score_per_object,
            legacy_score_base_multiplier,
            maximum_legacy_combo_score,
            ar,
            great_hit_window,
            ok_hit_window,
            meh_hit_window,
            hp,
            n_circles,
            n_sliders,
            n_large_ticks,
            n_spinners,
            stars,
            max_combo,
        } = self;

        assert_eq_float(*aim, expected.aim);
        assert_eq_float(
            *aim_difficult_slider_count,
            expected.aim_difficult_slider_count,
        );
        assert_eq_float(*speed, expected.speed);
        assert_eq_float(*flashlight, expected.flashlight);
        assert_eq_float(*slider_factor, expected.slider_factor);
        assert_eq_float(
            *aim_top_weighted_slider_factor,
            expected.aim_top_weighted_slider_factor,
        );
        assert_eq_float(
            *speed_top_weighted_slider_factor,
            expected.speed_top_weighted_slider_factor,
        );
        assert_eq_float(*speed_note_count, expected.speed_note_count);
        assert_eq_float(
            *aim_difficult_strain_count,
            expected.aim_difficult_strain_count,
        );
        assert_eq_float(
            *speed_difficult_strain_count,
            expected.speed_difficult_strain_count,
        );
        assert_eq_float(*nested_score_per_object, expected.nested_score_per_object);
        assert_eq_float(
            *legacy_score_base_multiplier,
            expected.legacy_score_base_multiplier,
        );
        assert_eq_float(
            *maximum_legacy_combo_score,
            expected.maximum_legacy_combo_score,
        );
        assert_eq_float(*ar, expected.ar);
        assert_eq_float(*great_hit_window, expected.great_hit_window);
        assert_eq_float(*ok_hit_window, expected.ok_hit_window);
        assert_eq_float(*meh_hit_window, expected.meh_hit_window);
        assert_eq_float(*hp, expected.hp);
        assert_eq!(*n_circles, expected.n_circles);
        assert_eq!(*n_sliders, expected.n_sliders);
        assert_eq!(*n_large_ticks, expected.n_large_ticks);
        assert_eq!(*n_spinners, expected.n_spinners);
        assert_eq_float(*stars, expected.stars);
        assert_eq!(*max_combo, expected.max_combo);
    }
}

impl AssertEq for TaikoDifficultyAttributes {
    fn assert_eq(&self, expected: &Self) {
        let Self {
            stamina,
            rhythm,
            color,
            reading,
            great_hit_window,
            ok_hit_window,
            mono_stamina_factor,
            mechanical_difficulty,
            consistency_factor,
            stars,
            max_combo,
            is_convert,
        } = self;

        assert_eq_float(*stamina, expected.stamina);
        assert_eq_float(*rhythm, expected.rhythm);
        assert_eq_float(*color, expected.color);
        assert_eq_float(*reading, expected.reading);
        assert_eq_float(*great_hit_window, expected.great_hit_window);
        assert_eq_float(*ok_hit_window, expected.ok_hit_window);
        assert_eq_float(*mono_stamina_factor, expected.mono_stamina_factor);
        assert_eq_float(*mechanical_difficulty, expected.mechanical_difficulty);
        assert_eq_float(*consistency_factor, expected.consistency_factor);
        assert_eq_float(*stars, expected.stars);
        assert_eq!(*max_combo, expected.max_combo);
        assert_eq!(*is_convert, expected.is_convert);
    }
}

impl AssertEq for CatchDifficultyAttributes {
    fn assert_eq(&self, expected: &Self) {
        let Self {
            stars,
            preempt,
            n_fruits,
            n_droplets,
            n_tiny_droplets,
            is_convert,
        } = self;

        assert_eq_float(*stars, expected.stars);
        assert_eq_float(*preempt, expected.preempt);
        assert_eq!(*n_fruits, expected.n_fruits);
        assert_eq!(*n_droplets, expected.n_droplets);
        assert_eq!(*n_tiny_droplets, expected.n_tiny_droplets);
        assert_eq!(*is_convert, expected.is_convert);
    }
}

impl AssertEq for ManiaDifficultyAttributes {
    fn assert_eq(&self, expected: &Self) {
        let Self {
            stars,
            n_objects,
            n_hold_notes,
            max_combo,
            is_convert,
        } = self;

        assert_eq_float(*stars, expected.stars);
        assert_eq!(*n_objects, expected.n_objects);
        assert_eq!(*n_hold_notes, expected.n_hold_notes);
        assert_eq!(*max_combo, expected.max_combo);
        assert_eq!(*is_convert, expected.is_convert);
    }
}