Skip to main content

rosu_pp/model/
mods.rs

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