1use rosu_map::section::general::GameMode;
2
3use self::calculator::ManiaPerformanceCalculator;
4
5pub use self::inspect::InspectManiaPerformance;
6
7use crate::{
8 Performance,
9 any::{
10 CalculateError, Difficulty, HitResultGenerator, HitResultPriority, InspectablePerformance,
11 IntoModePerformance, IntoPerformance, hitresult_generator::Fast,
12 },
13 mania::ManiaHitResults,
14 model::{mode::ConvertError, mods::GameMods},
15 osu::OsuPerformance,
16 util::map_or_attrs::MapOrAttrs,
17};
18
19use super::{Mania, attributes::ManiaPerformanceAttributes, score_state::ManiaScoreState};
20
21mod calculator;
22pub mod gradual;
23mod hitresult_generator;
24mod inspect;
25
26#[derive(Clone, Debug)]
28#[must_use]
29pub struct ManiaPerformance<'map> {
30 map_or_attrs: MapOrAttrs<'map, Mania>,
31 difficulty: Difficulty,
32 n320: Option<u32>,
33 n300: Option<u32>,
34 n200: Option<u32>,
35 n100: Option<u32>,
36 n50: Option<u32>,
37 misses: Option<u32>,
38 acc: Option<f64>,
39 hitresult_priority: HitResultPriority,
40 hitresult_generator: Option<fn(InspectManiaPerformance<'_>) -> ManiaHitResults>,
41}
42
43impl PartialEq for ManiaPerformance<'_> {
45 fn eq(&self, other: &Self) -> bool {
46 let Self {
47 map_or_attrs,
48 difficulty,
49 n320,
50 n300,
51 n200,
52 n100,
53 n50,
54 misses,
55 acc,
56 hitresult_priority,
57 hitresult_generator: _,
58 } = self;
59
60 map_or_attrs == &other.map_or_attrs
61 && difficulty == &other.difficulty
62 && n320 == &other.n320
63 && n300 == &other.n300
64 && n200 == &other.n200
65 && n100 == &other.n100
66 && n50 == &other.n50
67 && misses == &other.misses
68 && acc == &other.acc
69 && hitresult_priority == &other.hitresult_priority
70 }
71}
72
73impl<'map> ManiaPerformance<'map> {
74 pub fn new(map_or_attrs: impl IntoModePerformance<'map, Mania>) -> Self {
92 map_or_attrs.into_performance()
93 }
94
95 pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
106 if let Performance::Mania(calc) = map_or_attrs.into_performance() {
107 Some(calc)
108 } else {
109 None
110 }
111 }
112
113 pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
124 self.difficulty = self.difficulty.mods(mods);
125
126 self
127 }
128
129 pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
131 self.difficulty = difficulty;
132
133 self
134 }
135
136 pub fn passed_objects(mut self, passed_objects: u32) -> Self {
144 self.difficulty = self.difficulty.passed_objects(passed_objects);
145
146 self
147 }
148
149 pub fn clock_rate(mut self, clock_rate: f64) -> Self {
158 self.difficulty = self.difficulty.clock_rate(clock_rate);
159
160 self
161 }
162
163 pub fn hp(mut self, hp: f32, with_mods: bool) -> Self {
173 self.difficulty = self.difficulty.hp(hp, with_mods);
174
175 self
176 }
177
178 pub fn od(mut self, od: f32, with_mods: bool) -> Self {
188 self.difficulty = self.difficulty.od(od, with_mods);
189
190 self
191 }
192
193 pub fn accuracy(mut self, acc: f64) -> Self {
196 self.acc = Some(acc.clamp(0.0, 100.0) / 100.0);
197
198 self
199 }
200
201 pub const fn hitresult_priority(mut self, priority: HitResultPriority) -> Self {
203 self.hitresult_priority = priority;
204
205 self
206 }
207
208 pub fn hitresult_generator<H: HitResultGenerator<Mania>>(self) -> ManiaPerformance<'map> {
210 ManiaPerformance {
211 map_or_attrs: self.map_or_attrs,
212 difficulty: self.difficulty,
213 n320: self.n320,
214 n300: self.n300,
215 n200: self.n200,
216 n100: self.n100,
217 n50: self.n50,
218 misses: self.misses,
219 acc: self.acc,
220 hitresult_priority: self.hitresult_priority,
221 hitresult_generator: Some(H::generate_hitresults),
222 }
223 }
224
225 pub fn lazer(mut self, lazer: bool) -> Self {
236 self.difficulty = self.difficulty.lazer(lazer);
237
238 self
239 }
240
241 pub const fn n320(mut self, n320: u32) -> Self {
243 self.n320 = Some(n320);
244
245 self
246 }
247
248 pub const fn n300(mut self, n300: u32) -> Self {
250 self.n300 = Some(n300);
251
252 self
253 }
254
255 pub const fn n200(mut self, n200: u32) -> Self {
257 self.n200 = Some(n200);
258
259 self
260 }
261
262 pub const fn n100(mut self, n100: u32) -> Self {
264 self.n100 = Some(n100);
265
266 self
267 }
268
269 pub const fn n50(mut self, n50: u32) -> Self {
271 self.n50 = Some(n50);
272
273 self
274 }
275
276 pub const fn misses(mut self, n_misses: u32) -> Self {
278 self.misses = Some(n_misses);
279
280 self
281 }
282
283 #[expect(clippy::needless_pass_by_value, reason = "more sensible")]
285 pub const fn state(mut self, state: ManiaScoreState) -> Self {
286 let ManiaScoreState {
287 n320,
288 n300,
289 n200,
290 n100,
291 n50,
292 misses,
293 } = state;
294
295 self.n320 = Some(n320);
296 self.n300 = Some(n300);
297 self.n200 = Some(n200);
298 self.n100 = Some(n100);
299 self.n50 = Some(n50);
300 self.misses = Some(misses);
301
302 self
303 }
304
305 pub fn generate_state(&mut self) -> Result<ManiaScoreState, ConvertError> {
314 self.map_or_attrs.insert_attrs(&self.difficulty)?;
315
316 let state = unsafe { generate_state(self) };
318
319 Ok(state)
320 }
321
322 pub fn checked_generate_state(&mut self) -> Result<ManiaScoreState, CalculateError> {
325 self.map_or_attrs.checked_insert_attrs(&self.difficulty)?;
326
327 let state = unsafe { generate_state(self) };
329
330 Ok(state)
331 }
332
333 pub fn calculate(mut self) -> Result<ManiaPerformanceAttributes, ConvertError> {
335 let state = self.generate_state()?;
336
337 let attrs = unsafe { self.map_or_attrs.into_attrs() };
339
340 Ok(ManiaPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
341 }
342
343 pub fn checked_calculate(mut self) -> Result<ManiaPerformanceAttributes, CalculateError> {
346 let state = self.checked_generate_state()?;
347
348 let attrs = unsafe { self.map_or_attrs.into_attrs() };
350
351 Ok(ManiaPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
352 }
353
354 pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Mania>) -> Self {
355 Self {
356 map_or_attrs,
357 difficulty: Difficulty::new(),
358 n320: None,
359 n300: None,
360 n200: None,
361 n100: None,
362 n50: None,
363 misses: None,
364 acc: None,
365 hitresult_priority: HitResultPriority::DEFAULT,
366 hitresult_generator: None,
367 }
368 }
369}
370
371impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
372 type Error = OsuPerformance<'map>;
373
374 fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
380 let mods = osu.difficulty.get_mods();
381
382 let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Mania, mods) {
383 Ok(map) => map,
384 Err(map_or_attrs) => {
385 osu.map_or_attrs = map_or_attrs;
386
387 return Err(osu);
388 }
389 };
390
391 let OsuPerformance {
392 map_or_attrs: _,
393 difficulty,
394 acc,
395 combo: _,
396 large_tick_hits: _,
397 small_tick_hits: _,
398 slider_end_hits: _,
399 n300,
400 n100,
401 n50,
402 misses,
403 hitresult_priority,
404 hitresult_generator: _,
405 legacy_total_score: _,
406 } = osu;
407
408 Ok(Self {
409 map_or_attrs: MapOrAttrs::Map(map),
410 difficulty,
411 n320: None,
412 n300,
413 n200: None,
414 n100,
415 n50,
416 misses,
417 acc,
418 hitresult_priority,
419 hitresult_generator: None,
420 })
421 }
422}
423
424impl<'map, T: IntoModePerformance<'map, Mania>> From<T> for ManiaPerformance<'map> {
425 fn from(into: T) -> Self {
426 into.into_performance()
427 }
428}
429
430unsafe fn generate_state(perf: &mut ManiaPerformance) -> ManiaScoreState {
433 let attrs = unsafe { perf.map_or_attrs.get_attrs() };
435
436 let inspect = Mania::inspect_performance(perf, attrs);
437
438 let total_hits = inspect.total_hits();
439
440 let mut hitresults = match perf.hitresult_generator {
441 Some(generator) => generator(inspect),
442 None => <Fast as HitResultGenerator<Mania>>::generate_hitresults(inspect),
444 };
445
446 let remain = total_hits.saturating_sub(hitresults.total_hits());
447
448 match perf.hitresult_priority {
449 HitResultPriority::BestCase => {
450 match (perf.n320, perf.n300, perf.n200, perf.n100, perf.n50) {
451 (None, ..) => hitresults.n320 += remain,
452 (_, None, ..) => hitresults.n300 += remain,
453 (_, _, None, ..) => hitresults.n200 += remain,
454 (.., None, _) => hitresults.n100 += remain,
455 _ => hitresults.n50 += remain,
456 }
457 }
458 HitResultPriority::WorstCase => {
459 match (perf.n50, perf.n100, perf.n200, perf.n300, perf.n320) {
460 (None, ..) => hitresults.n50 += remain,
461 (_, None, ..) => hitresults.n100 += remain,
462 (_, _, None, ..) => hitresults.n200 += remain,
463 (.., None, _) => hitresults.n300 += remain,
464 _ => hitresults.n320 += remain,
465 }
466 }
467 }
468
469 let ManiaHitResults {
470 n320,
471 n300,
472 n200,
473 n100,
474 n50,
475 misses,
476 } = &hitresults;
477
478 perf.n320 = Some(*n320);
479 perf.n300 = Some(*n300);
480 perf.n200 = Some(*n200);
481 perf.n100 = Some(*n100);
482 perf.n50 = Some(*n50);
483 perf.misses = Some(*misses);
484
485 hitresults
486}
487
488#[cfg(test)]
489mod tests {
490 use std::sync::OnceLock;
491
492 use rosu_map::section::general::GameMode;
493 use rosu_mods::{GameMod, generated_mods::ClassicMania};
494
495 use crate::{
496 Beatmap,
497 any::{DifficultyAttributes, PerformanceAttributes},
498 mania::ManiaDifficultyAttributes,
499 osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
500 };
501
502 use super::*;
503
504 static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new();
505
506 const N_OBJECTS: u32 = 594;
507 const N_HOLD_NOTES: u32 = 121;
508
509 fn beatmap() -> Beatmap {
510 Beatmap::from_path("./resources/1638954.osu").unwrap()
511 }
512
513 fn attrs() -> ManiaDifficultyAttributes {
514 ATTRS
515 .get_or_init(|| {
516 let map = beatmap();
517 let attrs = Difficulty::new().calculate_for_mode::<Mania>(&map).unwrap();
518
519 assert_eq!(N_OBJECTS, map.hit_objects.len() as u32);
520 assert_eq!(
521 N_HOLD_NOTES,
522 map.hit_objects.iter().filter(|h| !h.is_circle()).count() as u32
523 );
524
525 attrs
526 })
527 .to_owned()
528 }
529
530 fn mods(classic: bool) -> rosu_mods::GameMods {
533 if classic {
534 let mut mods = rosu_mods::GameMods::new();
535 mods.insert(GameMod::ClassicMania(ClassicMania::default()));
536
537 mods
538 } else {
539 rosu_mods::GameMods::new()
540 }
541 }
542
543 #[test]
544 fn hitresults_n320_misses_best() {
545 let classic = true;
546
547 let state = ManiaPerformance::from(attrs())
548 .lazer(!classic)
549 .mods(mods(classic))
550 .n320(500)
551 .misses(2)
552 .hitresult_priority(HitResultPriority::BestCase)
553 .generate_state()
554 .unwrap();
555
556 let expected = ManiaScoreState {
557 n320: 500,
558 n300: 92,
559 n200: 0,
560 n100: 0,
561 n50: 0,
562 misses: 2,
563 };
564
565 assert_eq!(state, expected);
566 }
567
568 #[test]
569 fn hitresults_n100_n50_misses_worst() {
570 let classic = true;
571
572 let state = ManiaPerformance::from(attrs())
573 .lazer(!classic)
574 .mods(mods(classic))
575 .n100(200)
576 .n50(50)
577 .misses(2)
578 .hitresult_priority(HitResultPriority::WorstCase)
579 .generate_state()
580 .unwrap();
581
582 let expected = ManiaScoreState {
583 n320: 0,
584 n300: 0,
585 n200: 342,
586 n100: 200,
587 n50: 50,
588 misses: 2,
589 };
590
591 assert_eq!(state, expected);
592 }
593
594 #[test]
595 fn create() {
596 let mut map = beatmap();
597
598 let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
599 let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
600 let _ = ManiaPerformance::new(&map);
601 let _ = ManiaPerformance::new(map.clone());
602
603 let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
604 let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
605 let _ = ManiaPerformance::try_new(DifficultyAttributes::Mania(
606 ManiaDifficultyAttributes::default(),
607 ))
608 .unwrap();
609 let _ = ManiaPerformance::try_new(PerformanceAttributes::Mania(
610 ManiaPerformanceAttributes::default(),
611 ))
612 .unwrap();
613 let _ = ManiaPerformance::try_new(&map).unwrap();
614 let _ = ManiaPerformance::try_new(map.clone()).unwrap();
615
616 let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
617 let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
618 let _ = ManiaPerformance::from(&map);
619 let _ = ManiaPerformance::from(map.clone());
620
621 let _ = ManiaDifficultyAttributes::default().performance();
622 let _ = ManiaPerformanceAttributes::default().performance();
623
624 assert!(
625 map.convert_mut(GameMode::Osu, &GameMods::default())
626 .is_err()
627 );
628
629 assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
630 assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
631 assert!(
632 ManiaPerformance::try_new(
633 DifficultyAttributes::Osu(OsuDifficultyAttributes::default())
634 )
635 .is_none()
636 );
637 assert!(
638 ManiaPerformance::try_new(PerformanceAttributes::Osu(
639 OsuPerformanceAttributes::default()
640 ))
641 .is_none()
642 );
643 }
644}