rosu_pp/model/
mods.rs

1use std::fmt::{Debug, Formatter, Result as FmtResult};
2
3use rosu_mods::{
4    generated_mods::{
5        DifficultyAdjustCatch, DifficultyAdjustMania, DifficultyAdjustOsu, DifficultyAdjustTaiko,
6    },
7    GameMod, GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy,
8};
9
10/// Re-exported [`rosu_mods`].
11pub mod rosu_mods {
12    pub use rosu_mods::*;
13}
14
15/// Collection of game mods.
16///
17/// A convenient way to create [`GameMods`] is through its `From<T>`
18/// implementations where `T` can be
19/// - `u32`
20/// - [`rosu_mods::GameModsLegacy`]
21/// - [`rosu_mods::GameMods`]
22/// - [`rosu_mods::GameModsIntermode`]
23/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
24///
25/// # Example
26///
27/// ```
28/// use rosu_pp::GameMods;
29/// use rosu_mods::{GameModsIntermode, GameModsLegacy, GameMods as GameModsLazer};
30///
31/// let int = GameMods::from(64 + 8);
32/// let legacy = GameMods::from(GameModsLegacy::Hidden | GameModsLegacy::Easy);
33/// let lazer = GameMods::from(GameModsLazer::new());
34/// let intermode = GameMods::from(GameModsIntermode::new());
35/// ```
36#[derive(Clone, PartialEq)]
37pub enum GameMods {
38    Lazer(GameModsLazer),
39    Intermode(GameModsIntermode),
40    Legacy(GameModsLegacy),
41}
42
43impl Debug for GameMods {
44    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
45        match self {
46            Self::Lazer(ref mods) => Debug::fmt(mods, f),
47            Self::Intermode(ref mods) => Debug::fmt(mods, f),
48            Self::Legacy(ref mods) => Debug::fmt(mods, f),
49        }
50    }
51}
52
53impl GameMods {
54    pub(crate) const DEFAULT: Self = Self::Legacy(GameModsLegacy::NoMod);
55
56    /// Returns the mods' clock rate.
57    ///
58    /// In case of variable clock rates like for `WindUp`, this will return
59    /// `1.0`.
60    pub(crate) fn clock_rate(&self) -> f64 {
61        match self {
62            Self::Lazer(ref mods) => mods
63                .iter()
64                .find_map(|m| {
65                    let default = match m.intermode() {
66                        GameModIntermode::DoubleTime | GameModIntermode::HalfTime => {
67                            return m.clock_rate()
68                        }
69                        GameModIntermode::Nightcore => 1.5,
70                        GameModIntermode::Daycore => 0.75,
71                        _ => return None,
72                    };
73
74                    Some(default * (m.clock_rate()? / default))
75                })
76                .unwrap_or(1.0),
77            Self::Intermode(ref mods) => mods.legacy_clock_rate(),
78            Self::Legacy(mods) => mods.clock_rate(),
79        }
80    }
81
82    pub(crate) fn od_ar_hp_multiplier(&self) -> f64 {
83        if self.hr() {
84            1.4
85        } else if self.ez() {
86            0.5
87        } else {
88            1.0
89        }
90    }
91
92    /// Check whether the mods enable `hardrock_offsets`.
93    pub(crate) fn hardrock_offsets(&self) -> bool {
94        fn custom_hardrock_offsets(mods: &GameMods) -> Option<bool> {
95            match mods {
96                GameMods::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
97                    GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch {
98                        hard_rock_offsets,
99                        ..
100                    }) => *hard_rock_offsets,
101                    _ => None,
102                }),
103                GameMods::Intermode(_) | GameMods::Legacy(_) => None,
104            }
105        }
106
107        custom_hardrock_offsets(self).unwrap_or_else(|| self.hr())
108    }
109
110    pub(crate) fn no_slider_head_acc(&self, lazer: bool) -> bool {
111        match self {
112            Self::Lazer(ref mods) => mods
113                .iter()
114                .find_map(|m| match m {
115                    GameMod::ClassicOsu(cl) => Some(cl.no_slider_head_accuracy.unwrap_or(true)),
116                    _ => None,
117                })
118                .unwrap_or(!lazer),
119            Self::Intermode(ref mods) => mods.contains(GameModIntermode::Classic) || !lazer,
120            Self::Legacy(_) => !lazer,
121        }
122    }
123
124    pub(crate) fn reflection(&self) -> Reflection {
125        match self {
126            Self::Lazer(ref mods) => mods
127                .iter()
128                .find_map(|m| match m {
129                    GameMod::HardRockOsu(_) => Some(Reflection::Vertical),
130                    GameMod::MirrorOsu(mr) => match mr.reflection.as_deref() {
131                        None => Some(Reflection::Horizontal),
132                        Some("1") => Some(Reflection::Vertical),
133                        Some("2") => Some(Reflection::Both),
134                        Some(_) => Some(Reflection::None),
135                    },
136                    GameMod::MirrorCatch(_) => Some(Reflection::Horizontal),
137                    _ => None,
138                })
139                .unwrap_or(Reflection::None),
140            Self::Intermode(ref mods) => {
141                if mods.contains(GameModIntermode::HardRock) {
142                    Reflection::Vertical
143                } else {
144                    Reflection::None
145                }
146            }
147            Self::Legacy(mods) => {
148                if mods.contains(GameModsLegacy::HardRock) {
149                    Reflection::Vertical
150                } else {
151                    Reflection::None
152                }
153            }
154        }
155    }
156
157    pub(crate) fn mania_keys(&self) -> Option<f32> {
158        match self {
159            Self::Lazer(ref mods) => {
160                if mods.contains_intermode(GameModIntermode::OneKey) {
161                    Some(1.0)
162                } else if mods.contains_intermode(GameModIntermode::TwoKeys) {
163                    Some(2.0)
164                } else if mods.contains_intermode(GameModIntermode::ThreeKeys) {
165                    Some(3.0)
166                } else if mods.contains_intermode(GameModIntermode::FourKeys) {
167                    Some(4.0)
168                } else if mods.contains_intermode(GameModIntermode::FiveKeys) {
169                    Some(5.0)
170                } else if mods.contains_intermode(GameModIntermode::SixKeys) {
171                    Some(6.0)
172                } else if mods.contains_intermode(GameModIntermode::SevenKeys) {
173                    Some(7.0)
174                } else if mods.contains_intermode(GameModIntermode::EightKeys) {
175                    Some(8.0)
176                } else if mods.contains_intermode(GameModIntermode::NineKeys) {
177                    Some(9.0)
178                } else if mods.contains_intermode(GameModIntermode::TenKeys) {
179                    Some(10.0)
180                } else {
181                    None
182                }
183            }
184            Self::Intermode(ref mods) => {
185                if mods.contains(GameModIntermode::OneKey) {
186                    Some(1.0)
187                } else if mods.contains(GameModIntermode::TwoKeys) {
188                    Some(2.0)
189                } else if mods.contains(GameModIntermode::ThreeKeys) {
190                    Some(3.0)
191                } else if mods.contains(GameModIntermode::FourKeys) {
192                    Some(4.0)
193                } else if mods.contains(GameModIntermode::FiveKeys) {
194                    Some(5.0)
195                } else if mods.contains(GameModIntermode::SixKeys) {
196                    Some(6.0)
197                } else if mods.contains(GameModIntermode::SevenKeys) {
198                    Some(7.0)
199                } else if mods.contains(GameModIntermode::EightKeys) {
200                    Some(8.0)
201                } else if mods.contains(GameModIntermode::NineKeys) {
202                    Some(9.0)
203                } else if mods.contains(GameModIntermode::TenKeys) {
204                    Some(10.0)
205                } else {
206                    None
207                }
208            }
209            Self::Legacy(ref mods) => {
210                if mods.contains(GameModsLegacy::Key1) {
211                    Some(1.0)
212                } else if mods.contains(GameModsLegacy::Key2) {
213                    Some(2.0)
214                } else if mods.contains(GameModsLegacy::Key3) {
215                    Some(3.0)
216                } else if mods.contains(GameModsLegacy::Key4) {
217                    Some(4.0)
218                } else if mods.contains(GameModsLegacy::Key5) {
219                    Some(5.0)
220                } else if mods.contains(GameModsLegacy::Key6) {
221                    Some(6.0)
222                } else if mods.contains(GameModsLegacy::Key7) {
223                    Some(7.0)
224                } else if mods.contains(GameModsLegacy::Key8) {
225                    Some(8.0)
226                } else if mods.contains(GameModsLegacy::Key9) {
227                    Some(9.0)
228                } else {
229                    None
230                }
231            }
232        }
233    }
234
235    pub(crate) fn scroll_speed(&self) -> Option<f64> {
236        let Self::Lazer(mods) = self else { return None };
237
238        mods.iter()
239            .find_map(|m| match m {
240                GameMod::DifficultyAdjustTaiko(da) => Some(da.scroll_speed),
241                _ => None,
242            })
243            .flatten()
244    }
245
246    pub(crate) fn random_seed(&self) -> Option<i32> {
247        let Self::Lazer(mods) = self else { return None };
248
249        mods.iter()
250            .find_map(|m| match m {
251                // `RandomOsu` is not implemented because it relies on
252                // hitobjects' combo index which is never stored.
253                GameMod::RandomTaiko(m) => m.seed,
254                GameMod::RandomMania(m) => m.seed,
255                _ => None,
256            })
257            .map(|seed| seed as i32)
258    }
259}
260
261macro_rules! impl_map_attr {
262    ( $( $fn:ident: $field:ident [ $( $mode:ident ),* ] [$s:literal] ;)* ) => {
263        impl GameMods {
264            $(
265                #[doc = "Check whether the mods specify a custom "]
266                #[doc = $s]
267                #[doc = "value."]
268                pub(crate) fn $fn(&self) -> Option<f64> {
269                    match self {
270                        Self::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
271                            $( impl_map_attr!( @ $mode $field) => *$field, )*
272                            _ => None,
273                        }),
274                        Self::Intermode(_) | Self::Legacy(_) => None,
275                    }
276                }
277            )*
278        }
279    };
280
281    ( @ Osu $field:ident ) => { GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { $field, .. }) };
282    ( @ Taiko $field:ident ) => { GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko { $field, .. }) };
283    ( @ Catch $field:ident ) => { GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch { $field, .. }) };
284    ( @ Mania $field:ident ) => { GameMod::DifficultyAdjustMania(DifficultyAdjustMania { $field, .. }) };
285}
286
287impl_map_attr! {
288    ar: approach_rate [Osu, Catch] ["ar"];
289    cs: circle_size [Osu, Catch] ["cs"];
290    hp: drain_rate [Osu, Taiko, Catch, Mania] ["hp"];
291    od: overall_difficulty [Osu, Taiko, Catch, Mania] ["od"];
292}
293
294macro_rules! impl_has_mod {
295    ( $( $fn:ident: $is_legacy:tt $name:ident [ $s:literal ], )* ) => {
296        impl GameMods {
297            $(
298                // workaround for <https://github.com/rust-lang/rust-analyzer/issues/8092>
299                #[doc = "Check whether [`GameMods`] contain `"]
300                #[doc = $s]
301                #[doc = "`."]
302                pub(crate) fn $fn(&self) -> bool {
303                    match self {
304                        Self::Lazer(ref mods) => {
305                            mods.contains_intermode(GameModIntermode::$name)
306                        },
307                        Self::Intermode(ref mods) => {
308                            mods.contains(GameModIntermode::$name)
309                        },
310                        Self::Legacy(_mods) => {
311                            impl_has_mod!(LEGACY $is_legacy $name _mods)
312                        },
313                    }
314                }
315            )*
316        }
317    };
318
319    ( LEGACY + $name:ident $mods:ident ) => {
320        $mods.contains(GameModsLegacy::$name)
321    };
322
323    ( LEGACY - $name:ident $mods:ident ) => {
324        false
325    };
326}
327
328impl_has_mod! {
329    nf: + NoFail ["NoFail"],
330    ez: + Easy ["Easy"],
331    td: + TouchDevice ["TouchDevice"],
332    hd: + Hidden ["Hidden"],
333    hr: + HardRock ["HardRock"],
334    rx: + Relax ["Relax"],
335    fl: + Flashlight ["Flashlight"],
336    so: + SpunOut ["SpunOut"],
337    ap: + Autopilot ["Autopilot"],
338    bl: - Blinds ["Blinds"],
339    cl: - Classic ["Classic"],
340    invert: - Invert ["Invert"],
341    ho: - HoldOff ["HoldOff"],
342    tc: - Traceable ["Traceable"],
343}
344
345impl Default for GameMods {
346    fn default() -> Self {
347        Self::DEFAULT
348    }
349}
350
351impl From<GameModsLazer> for GameMods {
352    fn from(mods: GameModsLazer) -> Self {
353        Self::Lazer(mods)
354    }
355}
356
357impl From<GameModsIntermode> for GameMods {
358    fn from(mods: GameModsIntermode) -> Self {
359        Self::Intermode(mods)
360    }
361}
362
363impl From<&GameModsIntermode> for GameMods {
364    fn from(mods: &GameModsIntermode) -> Self {
365        // If only legacy mods are set, use `GameModsLegacy` and thus avoid
366        // allocating an owned `GameModsIntermode` instance.
367        match mods.checked_bits() {
368            Some(bits) => bits.into(),
369            None => mods.to_owned().into(),
370        }
371    }
372}
373
374impl From<GameModsLegacy> for GameMods {
375    fn from(mods: GameModsLegacy) -> Self {
376        Self::Legacy(mods)
377    }
378}
379
380impl From<u32> for GameMods {
381    fn from(bits: u32) -> Self {
382        GameModsLegacy::from_bits(bits).into()
383    }
384}
385
386#[derive(Copy, Clone, Debug, PartialEq, Eq)]
387pub(crate) enum Reflection {
388    None,
389    Vertical,
390    Horizontal,
391    Both,
392}