1use rosu_map::section::general::GameMode;
2
3use self::calculator::CatchPerformanceCalculator;
4
5use crate::{
6 Performance,
7 any::{
8 CalculateError, Difficulty, HitResultGenerator, InspectablePerformance,
9 IntoModePerformance, IntoPerformance, hitresult_generator::Fast,
10 },
11 catch::{CatchHitResults, performance::inspect::InspectCatchPerformance},
12 model::{mode::ConvertError, mods::GameMods},
13 osu::OsuPerformance,
14 util::map_or_attrs::MapOrAttrs,
15};
16
17use super::{Catch, attributes::CatchPerformanceAttributes, score_state::CatchScoreState};
18
19mod calculator;
20pub mod gradual;
21mod hitresult_generator;
22mod inspect;
23
24#[derive(Clone, Debug)]
26#[must_use]
27pub struct CatchPerformance<'map> {
28 map_or_attrs: MapOrAttrs<'map, Catch>,
29 difficulty: Difficulty,
30 acc: Option<f64>,
31 combo: Option<u32>,
32 fruits: Option<u32>,
33 droplets: Option<u32>,
34 tiny_droplets: Option<u32>,
35 tiny_droplet_misses: Option<u32>,
36 misses: Option<u32>,
37 hitresult_generator: Option<fn(InspectCatchPerformance<'_>) -> CatchHitResults>,
38}
39
40impl PartialEq for CatchPerformance<'_> {
42 fn eq(&self, other: &Self) -> bool {
43 let Self {
44 map_or_attrs,
45 difficulty,
46 acc,
47 combo,
48 fruits,
49 droplets,
50 tiny_droplets,
51 tiny_droplet_misses,
52 misses,
53 hitresult_generator: _,
54 } = self;
55
56 map_or_attrs == &other.map_or_attrs
57 && difficulty == &other.difficulty
58 && acc == &other.acc
59 && combo == &other.combo
60 && fruits == &other.fruits
61 && droplets == &other.droplets
62 && tiny_droplets == &other.tiny_droplets
63 && tiny_droplet_misses == &other.tiny_droplet_misses
64 && misses == &other.misses
65 }
66}
67
68impl<'map> CatchPerformance<'map> {
69 pub fn new(map_or_attrs: impl IntoModePerformance<'map, Catch>) -> Self {
87 map_or_attrs.into_performance()
88 }
89
90 pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
101 if let Performance::Catch(calc) = map_or_attrs.into_performance() {
102 Some(calc)
103 } else {
104 None
105 }
106 }
107
108 pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
119 self.difficulty = self.difficulty.mods(mods);
120
121 self
122 }
123
124 pub const fn combo(mut self, combo: u32) -> Self {
126 self.combo = Some(combo);
127
128 self
129 }
130
131 pub const fn fruits(mut self, n_fruits: u32) -> Self {
133 self.fruits = Some(n_fruits);
134
135 self
136 }
137
138 pub const fn droplets(mut self, n_droplets: u32) -> Self {
140 self.droplets = Some(n_droplets);
141
142 self
143 }
144
145 pub const fn tiny_droplets(mut self, n_tiny_droplets: u32) -> Self {
147 self.tiny_droplets = Some(n_tiny_droplets);
148
149 self
150 }
151
152 pub const fn tiny_droplet_misses(mut self, n_tiny_droplet_misses: u32) -> Self {
154 self.tiny_droplet_misses = Some(n_tiny_droplet_misses);
155
156 self
157 }
158
159 pub const fn misses(mut self, n_misses: u32) -> Self {
161 self.misses = Some(n_misses);
162
163 self
164 }
165
166 pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
168 self.difficulty = difficulty;
169
170 self
171 }
172
173 pub fn passed_objects(mut self, passed_objects: u32) -> Self {
181 self.difficulty = self.difficulty.passed_objects(passed_objects);
182
183 self
184 }
185
186 pub fn clock_rate(mut self, clock_rate: f64) -> Self {
195 self.difficulty = self.difficulty.clock_rate(clock_rate);
196
197 self
198 }
199
200 pub fn ar(mut self, ar: f32, fixed: bool) -> Self {
210 self.difficulty = self.difficulty.ar(ar, fixed);
211
212 self
213 }
214
215 pub fn cs(mut self, cs: f32, fixed: bool) -> Self {
225 self.difficulty = self.difficulty.cs(cs, fixed);
226
227 self
228 }
229
230 pub fn hp(mut self, hp: f32, fixed: bool) -> Self {
240 self.difficulty = self.difficulty.hp(hp, fixed);
241
242 self
243 }
244
245 pub fn od(mut self, od: f32, fixed: bool) -> Self {
255 self.difficulty = self.difficulty.od(od, fixed);
256
257 self
258 }
259
260 pub fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
262 self.difficulty = self.difficulty.hardrock_offsets(hardrock_offsets);
263
264 self
265 }
266
267 pub fn hitresult_generator<H: HitResultGenerator<Catch>>(self) -> CatchPerformance<'map> {
269 CatchPerformance {
270 map_or_attrs: self.map_or_attrs,
271 difficulty: self.difficulty,
272 acc: self.acc,
273 combo: self.combo,
274 fruits: self.fruits,
275 droplets: self.droplets,
276 tiny_droplets: self.tiny_droplets,
277 tiny_droplet_misses: self.tiny_droplet_misses,
278 misses: self.misses,
279 hitresult_generator: Some(H::generate_hitresults),
280 }
281 }
282
283 #[expect(clippy::needless_pass_by_value, reason = "more sensible")]
285 pub const fn state(mut self, state: CatchScoreState) -> Self {
286 let CatchScoreState {
287 max_combo,
288 hitresults,
289 } = state;
290
291 self.combo = Some(max_combo);
292
293 self.hitresults(hitresults)
294 }
295
296 pub const fn hitresults(mut self, hitresults: CatchHitResults) -> Self {
298 let CatchHitResults {
299 fruits: n_fruits,
300 droplets: n_droplets,
301 tiny_droplets: n_tiny_droplets,
302 tiny_droplet_misses: n_tiny_droplet_misses,
303 misses,
304 } = hitresults;
305
306 self.fruits = Some(n_fruits);
307 self.droplets = Some(n_droplets);
308 self.tiny_droplets = Some(n_tiny_droplets);
309 self.tiny_droplet_misses = Some(n_tiny_droplet_misses);
310 self.misses = Some(misses);
311
312 self
313 }
314
315 pub fn accuracy(mut self, acc: f64) -> Self {
318 self.acc = Some(acc.clamp(0.0, 100.0) / 100.0);
319
320 self
321 }
322
323 pub fn generate_state(&mut self) -> Result<CatchScoreState, ConvertError> {
332 self.map_or_attrs.insert_attrs(&self.difficulty)?;
333
334 let state = unsafe { generate_state(self) };
336
337 Ok(state)
338 }
339
340 pub fn checked_generate_state(&mut self) -> Result<CatchScoreState, CalculateError> {
343 self.map_or_attrs.checked_insert_attrs(&self.difficulty)?;
344
345 let state = unsafe { generate_state(self) };
347
348 Ok(state)
349 }
350
351 pub fn calculate(mut self) -> Result<CatchPerformanceAttributes, ConvertError> {
353 let state = self.generate_state()?;
354
355 let attrs = unsafe { self.map_or_attrs.into_attrs() };
357
358 Ok(CatchPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
359 }
360
361 pub fn checked_calculate(mut self) -> Result<CatchPerformanceAttributes, CalculateError> {
364 let state = self.checked_generate_state()?;
365
366 let attrs = unsafe { self.map_or_attrs.into_attrs() };
368
369 Ok(CatchPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
370 }
371
372 pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Catch>) -> Self {
373 Self {
374 map_or_attrs,
375 difficulty: Difficulty::new(),
376 acc: None,
377 combo: None,
378 fruits: None,
379 droplets: None,
380 tiny_droplets: None,
381 tiny_droplet_misses: None,
382 misses: None,
383 hitresult_generator: None,
384 }
385 }
386}
387
388impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
389 type Error = OsuPerformance<'map>;
390
391 fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
397 let mods = osu.difficulty.get_mods();
398
399 let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Catch, mods) {
400 Ok(map) => map,
401 Err(map_or_attrs) => {
402 osu.map_or_attrs = map_or_attrs;
403
404 return Err(osu);
405 }
406 };
407
408 let OsuPerformance {
409 map_or_attrs: _,
410 difficulty,
411 acc,
412 combo,
413 large_tick_hits: _,
414 small_tick_hits: _,
415 slider_end_hits: _,
416 n300,
417 n100,
418 n50,
419 misses,
420 hitresult_priority: _,
421 hitresult_generator: _,
422 legacy_total_score: _,
423 } = osu;
424
425 Ok(Self {
426 map_or_attrs: MapOrAttrs::Map(map),
427 difficulty,
428 acc,
429 combo,
430 fruits: n300,
431 droplets: n100,
432 tiny_droplets: n50,
433 tiny_droplet_misses: None,
434 misses,
435 hitresult_generator: None,
436 })
437 }
438}
439
440impl<'map, T: IntoModePerformance<'map, Catch>> From<T> for CatchPerformance<'map> {
441 fn from(into: T) -> Self {
442 into.into_performance()
443 }
444}
445
446unsafe fn generate_state(perf: &mut CatchPerformance) -> CatchScoreState {
449 let attrs = unsafe { perf.map_or_attrs.get_attrs() };
451
452 let inspect = Catch::inspect_performance(perf, attrs);
453
454 let misses = inspect.misses();
455 let max_combo = perf.combo.unwrap_or_else(|| attrs.max_combo() - misses);
456
457 let hitresults = match perf.hitresult_generator {
458 Some(generator) => generator(inspect),
459 None => <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect),
460 };
461
462 let CatchHitResults {
463 fruits,
464 droplets,
465 tiny_droplets,
466 tiny_droplet_misses,
467 misses,
468 } = hitresults;
469
470 perf.combo = Some(max_combo);
471 perf.fruits = Some(fruits);
472 perf.droplets = Some(droplets);
473 perf.tiny_droplets = Some(tiny_droplets);
474 perf.tiny_droplet_misses = Some(tiny_droplet_misses);
475 perf.misses = Some(misses);
476
477 CatchScoreState {
478 max_combo,
479 hitresults,
480 }
481}
482
483#[cfg(test)]
484mod test {
485 use std::sync::OnceLock;
486
487 use rosu_map::section::general::GameMode;
488
489 use crate::{
490 Beatmap,
491 any::{DifficultyAttributes, PerformanceAttributes},
492 catch::CatchDifficultyAttributes,
493 osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
494 };
495
496 use super::*;
497
498 static ATTRS: OnceLock<CatchDifficultyAttributes> = OnceLock::new();
499
500 const N_FRUITS: u32 = 728;
501 const N_DROPLETS: u32 = 2;
502 const N_TINY_DROPLETS: u32 = 263;
503
504 fn beatmap() -> Beatmap {
505 Beatmap::from_path("./resources/2118524.osu").unwrap()
506 }
507
508 fn attrs() -> CatchDifficultyAttributes {
509 ATTRS
510 .get_or_init(|| {
511 let map = beatmap();
512 let attrs = Difficulty::new().calculate_for_mode::<Catch>(&map).unwrap();
513
514 assert_eq!(N_FRUITS, attrs.n_fruits);
515 assert_eq!(N_DROPLETS, attrs.n_droplets);
516 assert_eq!(N_TINY_DROPLETS, attrs.n_tiny_droplets);
517
518 attrs
519 })
520 .to_owned()
521 }
522
523 #[test]
524 fn fruits_missing_objects() {
525 let state = CatchPerformance::from(attrs())
526 .fruits(N_FRUITS - 10)
527 .droplets(N_DROPLETS - 1)
528 .tiny_droplets(N_TINY_DROPLETS - 50)
529 .tiny_droplet_misses(20)
530 .misses(2)
531 .generate_state()
532 .unwrap();
533
534 let expected = CatchScoreState {
535 max_combo: N_FRUITS + N_DROPLETS - 2,
536 hitresults: {
537 CatchHitResults {
538 fruits: N_FRUITS - 2,
539 droplets: N_DROPLETS,
540 tiny_droplets: N_TINY_DROPLETS - 20,
541 tiny_droplet_misses: 20,
542 misses: 2,
543 }
544 },
545 };
546
547 assert_eq!(state, expected);
548 }
549
550 #[test]
551 fn create() {
552 let mut map = beatmap();
553
554 let _ = CatchPerformance::new(CatchDifficultyAttributes::default());
555 let _ = CatchPerformance::new(CatchPerformanceAttributes::default());
556 let _ = CatchPerformance::new(&map);
557 let _ = CatchPerformance::new(map.clone());
558
559 let _ = CatchPerformance::try_new(CatchDifficultyAttributes::default()).unwrap();
560 let _ = CatchPerformance::try_new(CatchPerformanceAttributes::default()).unwrap();
561 let _ = CatchPerformance::try_new(DifficultyAttributes::Catch(
562 CatchDifficultyAttributes::default(),
563 ))
564 .unwrap();
565 let _ = CatchPerformance::try_new(PerformanceAttributes::Catch(
566 CatchPerformanceAttributes::default(),
567 ))
568 .unwrap();
569 let _ = CatchPerformance::try_new(&map).unwrap();
570 let _ = CatchPerformance::try_new(map.clone()).unwrap();
571
572 let _ = CatchPerformance::from(CatchDifficultyAttributes::default());
573 let _ = CatchPerformance::from(CatchPerformanceAttributes::default());
574 let _ = CatchPerformance::from(&map);
575 let _ = CatchPerformance::from(map.clone());
576
577 let _ = CatchDifficultyAttributes::default().performance();
578 let _ = CatchPerformanceAttributes::default().performance();
579
580 assert!(
581 map.convert_mut(GameMode::Osu, &GameMods::default())
582 .is_err()
583 );
584
585 assert!(CatchPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
586 assert!(CatchPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
587 assert!(
588 CatchPerformance::try_new(
589 DifficultyAttributes::Osu(OsuDifficultyAttributes::default())
590 )
591 .is_none()
592 );
593 assert!(
594 CatchPerformance::try_new(PerformanceAttributes::Osu(
595 OsuPerformanceAttributes::default()
596 ))
597 .is_none()
598 );
599 }
600}