1use rosu_map::section::general::GameMode;
2
3use crate::{
4 Difficulty, GameMods,
5 any::{CalculateError, HitResultGenerator},
6 catch::{Catch, CatchPerformance},
7 mania::{Mania, ManiaPerformance},
8 model::beatmap::TooSuspicious,
9 osu::{Osu, OsuPerformance},
10 taiko::{Taiko, TaikoPerformance},
11};
12
13use self::into::IntoPerformance;
14
15use super::{attributes::PerformanceAttributes, score_state::ScoreState};
16
17pub mod gradual;
18pub mod inspectable;
19pub mod into;
20
21const NO_CONVERSION_REQUIRED: &str = "no conversion required";
22
23macro_rules! forward_to_variants {
24 ( $self:ident => |$perf:ident| $enum:ident($expr:expr) ) => {
25 match $self {
26 Self::Osu($perf) => $enum::Osu($expr),
27 Self::Taiko($perf) => $enum::Taiko($expr),
28 Self::Catch($perf) => $enum::Catch($expr),
29 Self::Mania($perf) => $enum::Mania($expr),
30 }
31 };
32}
33
34#[derive(Clone, Debug, PartialEq)]
36#[must_use]
37pub enum Performance<'map> {
38 Osu(OsuPerformance<'map>),
39 Taiko(TaikoPerformance<'map>),
40 Catch(CatchPerformance<'map>),
41 Mania(ManiaPerformance<'map>),
42}
43
44impl<'map> Performance<'map> {
45 pub fn new(map_or_attrs: impl IntoPerformance<'map>) -> Self {
66 map_or_attrs.into_performance()
67 }
68
69 #[expect(clippy::missing_panics_doc, reason = "unreachable")]
72 pub fn calculate(self) -> PerformanceAttributes {
73 forward_to_variants!(self => |perf| PerformanceAttributes(
74 perf.calculate().expect(NO_CONVERSION_REQUIRED)
75 ))
76 }
77
78 pub fn checked_calculate(self) -> Result<PerformanceAttributes, TooSuspicious> {
81 let map_err = |err| match err {
82 CalculateError::Suspicion(err) => err,
83 CalculateError::Convert(_) => unreachable!("{}", NO_CONVERSION_REQUIRED),
84 };
85
86 let this = match self {
87 Self::Osu(o) => PerformanceAttributes::Osu(o.checked_calculate().map_err(map_err)?),
88 Self::Taiko(t) => PerformanceAttributes::Taiko(t.checked_calculate().map_err(map_err)?),
89 Self::Catch(f) => PerformanceAttributes::Catch(f.checked_calculate().map_err(map_err)?),
90 Self::Mania(m) => PerformanceAttributes::Mania(m.checked_calculate().map_err(map_err)?),
91 };
92
93 Ok(this)
94 }
95
96 #[expect(clippy::result_large_err, reason = "both variants have the same size")]
107 pub fn try_mode(self, mode: GameMode) -> Result<Self, Self> {
108 match (self, mode) {
109 (Self::Osu(o), _) => o.try_mode(mode).map_err(Self::Osu),
110 (this @ Self::Taiko(_), GameMode::Taiko)
111 | (this @ Self::Catch(_), GameMode::Catch)
112 | (this @ Self::Mania(_), GameMode::Mania) => Ok(this),
113 (this, _) => Err(this),
114 }
115 }
116
117 pub fn mode_or_ignore(self, mode: GameMode) -> Self {
127 if let Self::Osu(osu) = self {
128 osu.mode_or_ignore(mode)
129 } else {
130 self
131 }
132 }
133
134 pub fn mods(self, mods: impl Into<GameMods>) -> Self {
145 forward_to_variants!(self => |perf| Self(perf.mods(mods)))
146 }
147
148 pub fn difficulty(self, difficulty: Difficulty) -> Self {
150 forward_to_variants!(self => |perf| Self(perf.difficulty(difficulty)))
151 }
152
153 pub fn passed_objects(self, passed_objects: u32) -> Self {
161 forward_to_variants!(self => |perf| Self(perf.passed_objects(passed_objects)))
162 }
163
164 pub fn clock_rate(self, clock_rate: f64) -> Self {
173 forward_to_variants!(self => |perf| Self(perf.clock_rate(clock_rate)))
174 }
175
176 pub fn ar(self, ar: f32, fixed: bool) -> Self {
188 match self {
189 Self::Osu(o) => Self::Osu(o.ar(ar, fixed)),
190 Self::Catch(c) => Self::Catch(c.ar(ar, fixed)),
191 Self::Taiko(_) | Self::Mania(_) => self,
192 }
193 }
194
195 pub fn cs(self, cs: f32, fixed: bool) -> Self {
207 match self {
208 Self::Osu(o) => Self::Osu(o.cs(cs, fixed)),
209 Self::Catch(c) => Self::Catch(c.cs(cs, fixed)),
210 Self::Taiko(_) | Self::Mania(_) => self,
211 }
212 }
213
214 pub fn hp(self, hp: f32, fixed: bool) -> Self {
224 forward_to_variants!(self => |perf| Self(perf.hp(hp, fixed)))
225 }
226
227 pub fn od(self, od: f32, fixed: bool) -> Self {
237 forward_to_variants!(self => |perf| Self(perf.od(od, fixed)))
238 }
239
240 pub fn hardrock_offsets(self, hardrock_offsets: bool) -> Self {
244 if let Self::Catch(catch) = self {
245 Self::Catch(catch.hardrock_offsets(hardrock_offsets))
246 } else {
247 self
248 }
249 }
250
251 pub fn state(self, state: ScoreState) -> Self {
253 forward_to_variants!(self => |perf| Self(perf.state(state.into())))
254 }
255
256 pub fn accuracy(self, acc: f64) -> Self {
258 forward_to_variants!(self => |perf| Self(perf.accuracy(acc)))
259 }
260
261 pub fn misses(self, n_misses: u32) -> Self {
263 forward_to_variants!(self => |perf| Self(perf.misses(n_misses)))
264 }
265
266 pub fn combo(self, combo: u32) -> Self {
270 match self {
271 Self::Osu(o) => Self::Osu(o.combo(combo)),
272 Self::Taiko(t) => Self::Taiko(t.combo(combo)),
273 Self::Catch(f) => Self::Catch(f.combo(combo)),
274 Self::Mania(_) => self,
275 }
276 }
277
278 pub fn hitresult_priority(self, priority: HitResultPriority) -> Self {
280 match self {
281 Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
282 Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
283 Self::Catch(_) => self,
284 Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
285 }
286 }
287
288 pub fn hitresult_generator<H>(self) -> Self
302 where
303 H: HitResultGenerator<Osu>
304 + HitResultGenerator<Taiko>
305 + HitResultGenerator<Catch>
306 + HitResultGenerator<Mania>,
307 {
308 forward_to_variants!(self => |perf| Self(perf.hitresult_generator::<H>()))
309 }
310
311 pub fn lazer(self, lazer: bool) -> Self {
321 match self {
322 Self::Osu(o) => Self::Osu(o.lazer(lazer)),
323 Self::Taiko(_) | Self::Catch(_) => self,
324 Self::Mania(m) => Self::Mania(m.lazer(lazer)),
325 }
326 }
327
328 pub fn large_tick_hits(self, large_tick_hits: u32) -> Self {
339 if let Self::Osu(osu) = self {
340 Self::Osu(osu.large_tick_hits(large_tick_hits))
341 } else {
342 self
343 }
344 }
345
346 pub fn small_tick_hits(self, small_tick_hits: u32) -> Self {
351 if let Self::Osu(osu) = self {
352 Self::Osu(osu.small_tick_hits(small_tick_hits))
353 } else {
354 self
355 }
356 }
357
358 pub fn slider_end_hits(self, slider_end_hits: u32) -> Self {
362 if let Self::Osu(osu) = self {
363 Self::Osu(osu.slider_end_hits(slider_end_hits))
364 } else {
365 self
366 }
367 }
368
369 pub fn n300(self, n300: u32) -> Self {
371 match self {
372 Self::Osu(o) => Self::Osu(o.n300(n300)),
373 Self::Taiko(t) => Self::Taiko(t.n300(n300)),
374 Self::Catch(f) => Self::Catch(f.fruits(n300)),
375 Self::Mania(m) => Self::Mania(m.n300(n300)),
376 }
377 }
378
379 pub fn n100(self, n100: u32) -> Self {
381 match self {
382 Self::Osu(o) => Self::Osu(o.n100(n100)),
383 Self::Taiko(t) => Self::Taiko(t.n100(n100)),
384 Self::Catch(f) => Self::Catch(f.droplets(n100)),
385 Self::Mania(m) => Self::Mania(m.n100(n100)),
386 }
387 }
388
389 pub fn n50(self, n50: u32) -> Self {
393 match self {
394 Self::Osu(o) => Self::Osu(o.n50(n50)),
395 Self::Taiko(_) => self,
396 Self::Catch(f) => Self::Catch(f.tiny_droplets(n50)),
397 Self::Mania(m) => Self::Mania(m.n50(n50)),
398 }
399 }
400
401 pub fn n_katu(self, n_katu: u32) -> Self {
406 match self {
407 Self::Osu(_) | Self::Taiko(_) => self,
408 Self::Catch(f) => Self::Catch(f.tiny_droplet_misses(n_katu)),
409 Self::Mania(m) => Self::Mania(m.n200(n_katu)),
410 }
411 }
412
413 pub fn n_geki(self, n_geki: u32) -> Self {
418 match self {
419 Self::Osu(_) | Self::Taiko(_) | Self::Catch(_) => self,
420 Self::Mania(m) => Self::Mania(m.n320(n_geki)),
421 }
422 }
423
424 pub fn legacy_total_score(self, legacy_total_score: u32) -> Self {
428 match self {
429 Self::Osu(o) => Self::Osu(o.legacy_total_score(legacy_total_score)),
430 _ => self,
431 }
432 }
433
434 #[expect(clippy::missing_panics_doc, reason = "unreachable")]
441 pub fn generate_state(&mut self) -> ScoreState {
442 match self {
443 Self::Osu(o) => o.generate_state().expect(NO_CONVERSION_REQUIRED).into(),
444 Self::Taiko(t) => t.generate_state().expect(NO_CONVERSION_REQUIRED).into(),
445 Self::Catch(f) => f.generate_state().expect(NO_CONVERSION_REQUIRED).into(),
446 Self::Mania(m) => m.generate_state().expect(NO_CONVERSION_REQUIRED).into(),
447 }
448 }
449
450 pub fn checked_generate_state(&mut self) -> Result<ScoreState, TooSuspicious> {
453 let map_err = |err| match err {
454 CalculateError::Suspicion(err) => err,
455 CalculateError::Convert(_) => unreachable!("{}", NO_CONVERSION_REQUIRED),
456 };
457
458 match self {
459 Self::Osu(o) => o.checked_generate_state().map(From::from).map_err(map_err),
460 Self::Taiko(t) => t.checked_generate_state().map(From::from).map_err(map_err),
461 Self::Catch(f) => f.checked_generate_state().map(From::from).map_err(map_err),
462 Self::Mania(m) => m.checked_generate_state().map(From::from).map_err(map_err),
463 }
464 }
465}
466
467#[derive(Copy, Clone, Debug, Eq, PartialEq)]
469pub enum HitResultPriority {
470 BestCase,
472 WorstCase,
474}
475
476impl HitResultPriority {
477 pub(crate) const DEFAULT: Self = Self::BestCase;
478}
479
480impl Default for HitResultPriority {
481 fn default() -> Self {
482 Self::DEFAULT
483 }
484}
485
486impl<'a, T: IntoPerformance<'a>> From<T> for Performance<'a> {
487 fn from(into: T) -> Self {
488 into.into_performance()
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use crate::{
495 Beatmap,
496 any::DifficultyAttributes,
497 catch::{CatchDifficultyAttributes, CatchPerformanceAttributes},
498 mania::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
499 osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
500 taiko::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
501 };
502
503 use super::*;
504
505 #[test]
506 fn create() {
507 let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
508
509 let _ = Performance::new(&map);
510 let _ = Performance::new(map.clone());
511
512 let _ = Performance::new(OsuDifficultyAttributes::default());
513 let _ = Performance::new(TaikoDifficultyAttributes::default());
514 let _ = Performance::new(CatchDifficultyAttributes::default());
515 let _ = Performance::new(ManiaDifficultyAttributes::default());
516
517 let _ = Performance::new(OsuPerformanceAttributes::default());
518 let _ = Performance::new(TaikoPerformanceAttributes::default());
519 let _ = Performance::new(CatchPerformanceAttributes::default());
520 let _ = Performance::new(ManiaPerformanceAttributes::default());
521
522 let _ = Performance::new(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
523 let _ = Performance::new(PerformanceAttributes::Taiko(
524 TaikoPerformanceAttributes::default(),
525 ));
526
527 let _ = Performance::from(&map);
528 let _ = Performance::from(map);
529
530 let _ = Performance::from(OsuDifficultyAttributes::default());
531 let _ = Performance::from(TaikoDifficultyAttributes::default());
532 let _ = Performance::from(CatchDifficultyAttributes::default());
533 let _ = Performance::from(ManiaDifficultyAttributes::default());
534
535 let _ = Performance::from(OsuPerformanceAttributes::default());
536 let _ = Performance::from(TaikoPerformanceAttributes::default());
537 let _ = Performance::from(CatchPerformanceAttributes::default());
538 let _ = Performance::from(ManiaPerformanceAttributes::default());
539
540 let _ = Performance::from(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
541 let _ = Performance::from(PerformanceAttributes::Taiko(
542 TaikoPerformanceAttributes::default(),
543 ));
544
545 let _ = DifficultyAttributes::Osu(OsuDifficultyAttributes::default()).performance();
546 let _ = PerformanceAttributes::Taiko(TaikoPerformanceAttributes::default()).performance();
547 }
548}