1use rosu_mods::{serde::GameModsSeed, GameModIntermode, GameModsIntermode};
2use serde::{
3 de::{DeserializeSeed, IgnoredAny},
4 Deserialize, Deserializer,
5};
6use serde_json::value::RawValue;
7use time::OffsetDateTime;
8
9use crate::{error::OsuError, request::GetUser, Osu, OsuResult};
10
11use super::{
12 beatmap::{BeatmapExtended, Beatmapset},
13 mods::GameMods,
14 serde_util,
15 user::User,
16 CacheUserFn, ContainedUsers, GameMode, Grade,
17};
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
21pub struct BeatmapUserScore {
22 #[serde(rename = "position")]
24 pub pos: usize,
25 pub score: Score,
27}
28
29impl BeatmapUserScore {
30 #[inline]
32 pub fn get_user<'o>(&self, osu: &'o Osu) -> GetUser<'o> {
33 self.score.get_user(osu)
34 }
35}
36
37impl ContainedUsers for BeatmapUserScore {
38 fn apply_to_users(&self, f: impl CacheUserFn) {
39 self.score.apply_to_users(f);
40 }
41}
42
43#[derive(Clone, Debug, Deserialize)]
44#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
45pub struct ProcessedScores {
46 pub scores: Vec<Score>,
47 #[serde(default)]
48 pub(crate) mode: Option<GameMode>,
49 #[serde(rename = "cursor_string")]
50 pub(crate) cursor: Box<str>,
51}
52
53impl ProcessedScores {
54 #[inline]
56 pub async fn get_next(&self, osu: &Osu) -> OsuResult<Self> {
57 let mut req = osu.scores().cursor(self.cursor.clone());
58
59 if let Some(mode) = self.mode {
60 req = req.mode(mode);
61 }
62
63 req.await
64 }
65}
66
67impl ContainedUsers for ProcessedScores {
68 fn apply_to_users(&self, f: impl CacheUserFn) {
69 self.scores.apply_to_users(f);
70 }
71}
72
73#[derive(Clone, Debug)]
74#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
75pub struct Score {
76 pub set_on_lazer: bool,
77 #[cfg_attr(feature = "serialize", serde(rename = "classic_total_score"))]
78 pub classic_score: u64,
79 pub ranked: Option<bool>,
80 pub preserve: Option<bool>,
81 pub processed: Option<bool>,
82 pub maximum_statistics: ScoreStatistics,
83 pub mods: GameMods,
84 pub statistics: ScoreStatistics,
85 #[cfg_attr(feature = "serialize", serde(rename = "beatmap_id"))]
86 pub map_id: u32,
87 pub best_id: Option<u64>,
88 pub id: u64,
89 #[cfg_attr(feature = "serialize", serde(rename = "rank"))]
90 pub grade: Grade,
91 #[cfg_attr(feature = "serialize", serde(rename = "type"))]
92 pub kind: Box<str>,
93 pub user_id: u32,
94 #[cfg_attr(feature = "serialize", serde(with = "serde_util::adjust_acc"))]
95 pub accuracy: f32,
96 pub build_id: Option<u32>,
97 #[cfg_attr(feature = "serialize", serde(with = "serde_util::datetime"))]
98 pub ended_at: OffsetDateTime,
99 pub has_replay: bool,
100 pub is_perfect_combo: bool,
101 pub legacy_perfect: Option<bool>,
102 pub legacy_score_id: Option<u64>,
103 #[cfg_attr(feature = "serialize", serde(rename = "legacy_total_score"))]
104 pub legacy_score: u32,
105 pub max_combo: u32,
106 pub passed: bool,
107 pub pp: Option<f32>,
108 #[cfg_attr(feature = "serialize", serde(rename = "ruleset_id"))]
109 pub mode: GameMode,
110 #[cfg_attr(feature = "serialize", serde(with = "serde_util::option_datetime"))]
111 pub started_at: Option<OffsetDateTime>,
112 #[cfg_attr(feature = "serialize", serde(rename = "total_score"))]
113 pub score: u32,
114 pub replay: bool,
115 pub current_user_attributes: UserAttributes,
116 pub total_score_without_mods: Option<u32>,
117 #[cfg_attr(feature = "serialize", serde(rename = "beatmap"))]
118 pub map: Option<Box<BeatmapExtended>>,
119 #[cfg_attr(feature = "serialize", serde(rename = "beatmapset"))]
120 pub mapset: Option<Box<Beatmapset>>,
121 pub rank_global: Option<u32>,
122 pub user: Option<Box<User>>,
123 pub weight: Option<ScoreWeight>,
124}
125
126impl ContainedUsers for Score {
127 fn apply_to_users(&self, f: impl CacheUserFn) {
128 self.user.apply_to_users(f);
129 self.map.apply_to_users(f);
130 self.mapset.apply_to_users(f);
131 }
132}
133
134impl<'de> Deserialize<'de> for Score {
135 #[allow(clippy::too_many_lines)]
136 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
137 #[derive(Deserialize)]
138 #[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
139 struct ScoreRawMods {
140 set_on_lazer: Option<bool>, #[serde(default, rename = "classic_total_score")]
142 classic_score: u64, ranked: Option<bool>,
144 preserve: Option<bool>,
145 processed: Option<bool>,
146 #[serde(default)]
147 maximum_statistics: ScoreStatistics,
148 mods: Box<RawValue>,
149 statistics: ScoreStatistics,
150 #[serde(rename = "beatmap_id")]
151 map_id: Option<u32>, best_id: Option<u64>,
153 id: u64,
154 #[serde(rename = "rank")]
155 grade: Grade,
156 #[serde(rename = "type")]
157 kind: Box<str>,
158 user_id: u32,
159 #[serde(with = "serde_util::adjust_acc")]
160 accuracy: f32,
161 build_id: Option<u32>,
162 #[serde(alias = "created_at", with = "serde_util::datetime")]
163 ended_at: OffsetDateTime,
164 has_replay: Option<bool>, is_perfect_combo: Option<bool>, #[serde(alias = "perfect")]
167 legacy_perfect: Option<bool>,
168 legacy_score_id: Option<u64>,
169 #[serde(default, rename = "legacy_total_score")]
170 legacy_score: u32, max_combo: u32,
172 passed: bool,
173 pp: Option<f32>,
174 #[serde(rename = "mode")]
175 mode_: Option<IgnoredAny>, #[serde(rename = "ruleset_id", alias = "mode_int")]
177 mode: GameMode,
178 #[serde(default, with = "serde_util::option_datetime")]
179 started_at: Option<OffsetDateTime>,
180 #[serde(rename = "total_score", alias = "score")]
181 score: u32,
182 replay: bool,
183 current_user_attributes: UserAttributes,
184 total_score_without_mods: Option<u32>,
185 #[serde(rename = "beatmap")]
186 map: Option<Box<BeatmapExtended>>,
187 #[serde(rename = "beatmapset")]
188 mapset: Option<Box<Beatmapset>>,
189 rank_global: Option<u32>,
190 user: Option<Box<User>>,
191 weight: Option<MaybeWeight>,
194 }
195
196 #[derive(Deserialize)]
197 struct MaybeWeight {
198 percentage: f32,
199 pp: Option<f32>,
200 }
201
202 let score_raw = <ScoreRawMods as serde::Deserialize>::deserialize(d)?;
203 let set_on_stable = score_raw.set_on_lazer.map_or(
204 score_raw.legacy_score > 0 || score_raw.mode_.is_some(),
205 <bool as std::ops::Not>::not,
206 );
207
208 Ok(Score {
209 set_on_lazer: !set_on_stable,
210 classic_score: score_raw.classic_score,
211 ranked: score_raw.ranked,
212 preserve: score_raw.preserve,
213 processed: score_raw.processed,
214 maximum_statistics: score_raw.maximum_statistics,
215 mods: GameModsSeed::Mode {
216 mode: score_raw.mode,
217 deny_unknown_fields: false,
218 }
219 .deserialize(&*score_raw.mods)
220 .map_err(|e| OsuError::invalid_mods(&score_raw.mods, &e))?,
221 statistics: score_raw.statistics,
222 map_id: score_raw
223 .map_id
224 .or_else(|| score_raw.map.as_ref().map(|map| map.map_id))
225 .unwrap_or(0),
226 best_id: score_raw.best_id,
227 id: score_raw.id,
228 grade: score_raw.grade,
229 kind: score_raw.kind,
230 user_id: score_raw.user_id,
231 accuracy: score_raw.accuracy,
232 build_id: score_raw.build_id,
233 ended_at: score_raw.ended_at,
234 has_replay: score_raw.has_replay.unwrap_or(score_raw.replay),
235 is_perfect_combo: score_raw
236 .is_perfect_combo
237 .or(score_raw.legacy_perfect)
238 .unwrap_or(false),
239 legacy_perfect: score_raw.legacy_perfect,
240 legacy_score_id: score_raw
241 .legacy_score_id
242 .or_else(|| set_on_stable.then_some(score_raw.id)),
243 legacy_score: if set_on_stable {
244 score_raw.score
245 } else {
246 score_raw.legacy_score
247 },
248 max_combo: score_raw.max_combo,
249 passed: score_raw.passed,
250 pp: score_raw.pp,
251 mode: score_raw.mode,
252 started_at: score_raw.started_at,
253 score: score_raw.score,
254 replay: score_raw.replay,
255 current_user_attributes: score_raw.current_user_attributes,
256 total_score_without_mods: score_raw.total_score_without_mods,
257 map: score_raw.map,
258 mapset: score_raw.mapset,
259 rank_global: score_raw.rank_global,
260 user: score_raw.user,
261 weight: score_raw.weight.and_then(|weight| {
262 Some(ScoreWeight {
263 percentage: weight.percentage,
264 pp: weight.pp?,
265 })
266 }),
267 })
268 }
269}
270
271impl Score {
272 #[inline]
273 pub fn get_user<'o>(&self, osu: &'o Osu) -> GetUser<'o> {
274 osu.user(self.user_id)
275 }
276
277 #[inline]
281 pub const fn total_hits(&self) -> u32 {
282 self.statistics.total_hits(self.mode)
283 }
284
285 #[inline]
287 pub fn accuracy(&self) -> f32 {
288 self.statistics
289 .accuracy(self.mode, &self.maximum_statistics)
290 }
291
292 #[inline]
296 pub fn legacy_accuracy(&self) -> f32 {
297 self.statistics.legacy_accuracy(self.mode)
298 }
299
300 pub fn grade(&self, accuracy: Option<f32>) -> Grade {
308 match self.mode {
309 GameMode::Osu => osu_grade(self, accuracy),
310 GameMode::Taiko => taiko_grade(self, accuracy),
311 GameMode::Catch => catch_grade(self, accuracy),
312 GameMode::Mania => mania_grade(self, accuracy),
313 }
314 }
315
316 pub fn legacy_grade(&self, accuracy: Option<f32>) -> Grade {
324 match self.mode {
325 GameMode::Osu => osu_grade_legacy(self),
326 GameMode::Taiko => taiko_grade_legacy(self),
327 GameMode::Catch => catch_grade_legacy(self, accuracy.ok_or(Score::legacy_accuracy)),
328 GameMode::Mania => mania_grade_legacy(self, accuracy.ok_or(Score::legacy_accuracy)),
329 }
330 }
331}
332
333impl PartialEq for Score {
334 #[inline]
335 fn eq(&self, other: &Self) -> bool {
336 self.user_id == other.user_id
337 && (self.ended_at.unix_timestamp() - other.ended_at.unix_timestamp()).abs() <= 2
338 && self.score == other.score
339 }
340}
341
342impl Eq for Score {}
343
344#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
345#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
346pub struct ScoreStatistics {
347 #[serde(
348 default,
349 alias = "count_miss",
350 deserialize_with = "serde_util::from_option::deserialize"
351 )]
352 pub miss: u32,
353 #[serde(
354 default,
355 alias = "count_50",
356 deserialize_with = "serde_util::from_option::deserialize"
357 )]
358 pub meh: u32,
359 #[serde(
360 default,
361 alias = "count_100",
362 deserialize_with = "serde_util::from_option::deserialize"
363 )]
364 pub ok: u32,
365 #[serde(
366 default,
367 alias = "count_katu",
368 deserialize_with = "serde_util::from_option::deserialize"
369 )]
370 pub good: u32,
371 #[serde(
372 default,
373 alias = "count_300",
374 deserialize_with = "serde_util::from_option::deserialize"
375 )]
376 pub great: u32,
377 #[serde(
378 default,
379 alias = "count_geki",
380 deserialize_with = "serde_util::from_option::deserialize"
381 )]
382 pub perfect: u32,
383 #[serde(default)]
384 pub large_tick_hit: u32,
385 #[serde(default)]
386 pub large_tick_miss: u32,
387 #[serde(default)]
388 pub small_tick_hit: u32,
389 #[serde(default)]
390 pub small_tick_miss: u32,
391 #[serde(default)]
392 pub ignore_hit: u32,
393 #[serde(default)]
394 pub ignore_miss: u32,
395 #[serde(default)]
396 pub large_bonus: u32,
397 #[serde(default)]
398 pub small_bonus: u32,
399 #[serde(default)]
400 pub slider_tail_hit: u32,
401 #[serde(default)]
402 pub combo_break: u32,
403 #[serde(default)]
404 pub legacy_combo_increase: u32,
405}
406
407impl ScoreStatistics {
408 pub const fn total_hits(&self, mode: GameMode) -> u32 {
412 match mode {
413 GameMode::Osu => self.ok + self.meh + self.great + self.miss,
414 GameMode::Taiko => self.ok + self.great + self.miss,
415 GameMode::Catch => self.miss + self.great + self.large_tick_hit + self.small_tick_hit,
416 GameMode::Mania => {
417 self.ok + self.meh + self.good + self.miss + self.great + self.perfect
418 }
419 }
420 }
421
422 pub fn accuracy(&self, mode: GameMode, max_statistics: &ScoreStatistics) -> f32 {
424 let numerator = self.accuracy_value(mode) as f32;
425 let denominator = max_statistics.accuracy_value(mode) as f32;
426
427 (10_000.0 * numerator / denominator).round() / 100.0
428 }
429
430 const fn accuracy_value(&self, mode: GameMode) -> u32 {
431 let mut sum = 0;
432
433 sum += match mode {
434 GameMode::Osu | GameMode::Taiko | GameMode::Mania => self.small_tick_hit * 10,
435 GameMode::Catch => self.small_tick_hit * 300,
436 };
437
438 sum += match mode {
439 GameMode::Osu | GameMode::Taiko | GameMode::Mania => self.large_tick_hit * 30,
440 GameMode::Catch => self.large_tick_hit * 300,
441 };
442
443 sum += self.slider_tail_hit * 150;
444 sum += self.meh * 50;
445
446 sum += match mode {
447 GameMode::Osu | GameMode::Catch | GameMode::Mania => self.ok * 100,
448 GameMode::Taiko => self.ok * 150,
449 };
450
451 sum += self.good * 200;
452 sum += self.great * 300;
453
454 sum += match mode {
455 GameMode::Osu | GameMode::Taiko | GameMode::Catch => self.perfect * 300,
456 GameMode::Mania => self.perfect * 305,
457 };
458
459 sum
460 }
461
462 pub fn legacy_accuracy(&self, mode: GameMode) -> f32 {
466 let numerator;
467 let denominator;
468
469 match mode {
470 GameMode::Osu => {
471 numerator = (self.meh * 50 + self.ok * 100 + self.great * 300) as f32;
472 denominator = (self.total_hits(mode) * 300) as f32;
473 }
474 GameMode::Taiko => {
475 numerator = (self.ok + self.great * 2) as f32;
476 denominator = (self.total_hits(mode) * 2) as f32;
477 }
478 GameMode::Catch => {
479 numerator = (self.large_tick_hit + self.great + self.small_tick_hit) as f32;
480 denominator = self.total_hits(mode) as f32;
481 }
482 GameMode::Mania => {
483 numerator = (self.meh * 50
484 + self.ok * 100
485 + self.good * 200
486 + (self.great + self.perfect) * 300) as f32;
487
488 denominator = (self.total_hits(mode) * 300) as f32;
489 }
490 }
491
492 (10_000.0 * numerator / denominator).round() / 100.0
493 }
494
495 pub const fn as_legacy(&self, mode: GameMode) -> LegacyScoreStatistics {
497 let mut count_geki = 0;
498 let mut count_katu = 0;
499 let count_300 = self.great;
500 let count_100;
501 let mut count_50 = 0;
502 let count_miss = self.miss;
503
504 match mode {
505 GameMode::Osu => {
506 count_100 = self.ok;
507 count_50 = self.meh;
508 }
509 GameMode::Taiko => count_100 = self.ok,
510 GameMode::Catch => {
511 const fn max(a: u32, b: u32) -> u32 {
512 if a > b {
513 a
514 } else {
515 b
516 }
517 }
518
519 count_100 = max(self.large_tick_hit, self.ok);
520 count_50 = max(self.small_tick_hit, self.meh);
521 count_katu = max(self.small_tick_miss, self.good);
522 }
523 GameMode::Mania => {
524 count_geki = self.perfect;
525 count_katu = self.good;
526 count_100 = self.ok;
527 count_50 = self.meh;
528 }
529 }
530
531 LegacyScoreStatistics {
532 count_geki,
533 count_katu,
534 count_300,
535 count_100,
536 count_50,
537 count_miss,
538 }
539 }
540}
541
542#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
543#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
544pub struct LegacyScoreStatistics {
545 pub count_geki: u32,
546 pub count_katu: u32,
547 pub count_300: u32,
548 pub count_100: u32,
549 pub count_50: u32,
550 pub count_miss: u32,
551}
552
553impl LegacyScoreStatistics {
554 pub fn total_hits(&self, mode: GameMode) -> u32 {
558 let mut amount = self.count_300 + self.count_100 + self.count_miss;
559
560 if mode != GameMode::Taiko {
561 amount += self.count_50;
562
563 if mode != GameMode::Osu {
564 amount += self.count_katu;
565 amount += u32::from(mode != GameMode::Catch) * self.count_geki;
566 }
567 }
568
569 amount
570 }
571
572 pub fn accuracy(&self, mode: GameMode) -> f32 {
574 let amount_objects = self.total_hits(mode) as f32;
575
576 let (numerator, denumerator) = match mode {
577 GameMode::Taiko => (
578 0.5 * self.count_100 as f32 + self.count_300 as f32,
579 amount_objects,
580 ),
581 GameMode::Catch => (
582 (self.count_300 + self.count_100 + self.count_50) as f32,
583 amount_objects,
584 ),
585 GameMode::Osu | GameMode::Mania => {
586 let mut n =
587 (self.count_50 * 50 + self.count_100 * 100 + self.count_300 * 300) as f32;
588
589 n += (u32::from(mode == GameMode::Mania)
590 * (self.count_katu * 200 + self.count_geki * 300)) as f32;
591
592 (n, amount_objects * 300.0)
593 }
594 };
595
596 (10_000.0 * numerator / denumerator).round() / 100.0
597 }
598}
599
600#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
601#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
602pub struct ScoreWeight {
603 pub percentage: f32,
605 pub pp: f32,
607}
608
609#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
610#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
611pub struct UserAttributes {
612 pub pin: Option<UserAttributesPin>,
613}
614
615#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
616#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
617pub struct UserAttributesPin {
618 pub is_pinned: bool,
619 pub score_id: u64,
620}
621
622fn hdfl() -> GameModsIntermode {
623 [GameModIntermode::Hidden, GameModIntermode::Flashlight]
624 .into_iter()
625 .collect()
626}
627
628fn hdflfi() -> GameModsIntermode {
629 [
630 GameModIntermode::Hidden,
631 GameModIntermode::Flashlight,
632 GameModIntermode::FadeIn,
633 ]
634 .into_iter()
635 .collect()
636}
637
638fn osu_grade(score: &Score, accuracy: Option<f32>) -> Grade {
639 if score.statistics.great == score.maximum_statistics.great
640 && score.statistics.large_tick_hit == score.maximum_statistics.large_tick_hit
641 && score.statistics.slider_tail_hit == score.maximum_statistics.slider_tail_hit
642 {
643 return if score.mods.contains_any(hdfl()) {
644 Grade::XH
645 } else {
646 Grade::X
647 };
648 }
649
650 let accuracy = accuracy.unwrap_or_else(|| score.accuracy());
651
652 if accuracy >= 95.0
653 && score.statistics.miss == 0
654 && score.statistics.large_tick_hit == score.maximum_statistics.large_tick_hit
655 {
656 if score.mods.contains_any(hdflfi()) {
657 Grade::SH
658 } else {
659 Grade::S
660 }
661 } else if accuracy >= 90.0 {
662 Grade::A
663 } else if accuracy >= 80.0 {
664 Grade::B
665 } else if accuracy >= 70.0 {
666 Grade::C
667 } else {
668 Grade::D
669 }
670}
671
672fn taiko_grade(score: &Score, accuracy: Option<f32>) -> Grade {
673 osu_grade(score, accuracy)
674}
675
676fn catch_grade(score: &Score, accuracy: Option<f32>) -> Grade {
677 catch_grade_legacy(score, accuracy.ok_or(Score::accuracy))
678}
679
680fn mania_grade(score: &Score, accuracy: Option<f32>) -> Grade {
681 mania_grade_legacy(score, accuracy.ok_or(Score::accuracy))
682}
683
684fn osu_grade_legacy(score: &Score) -> Grade {
685 if score.statistics.great == score.maximum_statistics.great {
686 return if score.mods.contains_any(hdfl()) {
687 Grade::XH
688 } else {
689 Grade::X
690 };
691 }
692
693 let stats = &score.statistics;
694 let passed_objects = stats.total_hits(GameMode::Osu);
695
696 let ratio300 = stats.great as f32 / passed_objects as f32;
697 let ratio50 = stats.meh as f32 / passed_objects as f32;
698
699 if ratio300 > 0.9 && ratio50 < 0.01 && stats.miss == 0 {
700 if score.mods.contains_any(hdfl()) {
701 Grade::SH
702 } else {
703 Grade::S
704 }
705 } else if ratio300 > 0.9 || (ratio300 > 0.8 && stats.miss == 0) {
706 Grade::A
707 } else if ratio300 > 0.8 || (ratio300 > 0.7 && stats.miss == 0) {
708 Grade::B
709 } else if ratio300 > 0.6 {
710 Grade::C
711 } else {
712 Grade::D
713 }
714}
715
716fn taiko_grade_legacy(score: &Score) -> Grade {
717 if score.statistics.great == score.maximum_statistics.great {
718 return if score.mods.contains_any(hdfl()) {
719 Grade::XH
720 } else {
721 Grade::X
722 };
723 }
724
725 let stats = &score.statistics;
726 let passed_objects = stats.total_hits(GameMode::Taiko);
727 let ratio300 = stats.great as f32 / passed_objects as f32;
728
729 if ratio300 > 0.9 && stats.miss == 0 {
730 if score.mods.contains_any(hdfl()) {
731 Grade::SH
732 } else {
733 Grade::S
734 }
735 } else if ratio300 > 0.9 || (ratio300 > 0.8 && stats.miss == 0) {
736 Grade::A
737 } else if ratio300 > 0.8 || (ratio300 > 0.7 && stats.miss == 0) {
738 Grade::B
739 } else if ratio300 > 0.6 {
740 Grade::C
741 } else {
742 Grade::D
743 }
744}
745
746fn catch_grade_legacy(score: &Score, accuracy: Result<f32, fn(&Score) -> f32>) -> Grade {
747 let accuracy = accuracy.unwrap_or_else(|f| f(score));
748
749 if (100.0 - accuracy).abs() < f32::EPSILON {
750 if score.mods.contains_any(hdfl()) {
751 Grade::XH
752 } else {
753 Grade::X
754 }
755 } else if accuracy >= 98.0 {
756 if score.mods.contains_any(hdfl()) {
757 Grade::SH
758 } else {
759 Grade::S
760 }
761 } else if accuracy >= 94.0 {
762 Grade::A
763 } else if accuracy >= 90.0 {
764 Grade::B
765 } else if accuracy >= 85.0 {
766 Grade::C
767 } else {
768 Grade::D
769 }
770}
771
772fn mania_grade_legacy(score: &Score, accuracy: Result<f32, fn(&Score) -> f32>) -> Grade {
773 if score.statistics.perfect == score.maximum_statistics.perfect {
774 return if score.mods.contains_any(hdflfi()) {
775 Grade::XH
776 } else {
777 Grade::X
778 };
779 }
780
781 let accuracy = accuracy.unwrap_or_else(|f| f(score));
782
783 if accuracy >= 95.0 {
784 if score.mods.contains_any(hdflfi()) {
785 Grade::SH
786 } else {
787 Grade::S
788 }
789 } else if accuracy >= 90.0 {
790 Grade::A
791 } else if accuracy >= 80.0 {
792 Grade::B
793 } else if accuracy >= 70.0 {
794 Grade::C
795 } else {
796 Grade::D
797 }
798}