rosu_mods/
intermode.rs

1use std::{
2    cmp::Ordering,
3    collections::BTreeSet,
4    convert::Infallible,
5    fmt::{Debug, Display, Formatter, Result as FmtResult},
6    ops::{BitOr, BitOrAssign, Sub, SubAssign},
7    str::FromStr,
8};
9
10use crate::{
11    generated_mods::{DoubleTimeOsu, NightcoreOsu, PerfectOsu, SuddenDeathOsu},
12    util, GameModsLegacy,
13};
14
15use crate::GameMode;
16
17use super::{
18    intersection::{GameModsIntermodeIntersection, IntersectionInner},
19    iter::{GameModsIntermodeIter, IntoGameModsIntermodeIter},
20    Acronym, GameMod, GameModIntermode, GameMods,
21};
22
23/// Combination of [`GameModIntermode`]s.
24#[derive(Clone, Default, PartialEq, Eq, Hash)]
25pub struct GameModsIntermode {
26    inner: BTreeSet<GameModIntermode>,
27}
28
29impl GameModsIntermode {
30    /// Returns empty mods i.e. "`NoMod`"
31    pub const fn new() -> Self {
32        Self {
33            inner: BTreeSet::new(),
34        }
35    }
36
37    /// Return the accumulated bit values of all contained mods.
38    ///
39    /// Mods that don't have bit values will be ignored.
40    /// See <https://github.com/ppy/osu-api/wiki#mods>
41    ///
42    /// # Example
43    /// ```rust
44    /// use rosu_mods::mods;
45    ///
46    /// let hdhrdtwu = mods!(HD HR DT WU);
47    /// assert_eq!(hdhrdtwu.bits(), 8 + 16 + 64);
48    /// ```
49    pub fn bits(&self) -> u32 {
50        self.inner
51            .iter()
52            .copied()
53            .filter_map(GameModIntermode::bits)
54            .fold(0, u32::bitor)
55    }
56
57    /// Return the accumulated bit values of all contained mods.
58    ///
59    /// If any contained mod has no bit value `None` is returned.
60    /// See <https://github.com/ppy/osu-api/wiki#mods>
61    ///
62    /// # Example
63    /// ```rust
64    /// use rosu_mods::mods;
65    ///
66    /// let hdhrdt = mods!(HD HR DT);
67    /// assert_eq!(hdhrdt.checked_bits(), Some(8 + 16 + 64));
68    ///
69    /// let hdhrdtwu = mods!(HD HR DT WU);
70    /// assert_eq!(hdhrdtwu.checked_bits(), None);
71    /// ```
72    pub fn checked_bits(&self) -> Option<u32> {
73        self.inner
74            .iter()
75            .copied()
76            .map(GameModIntermode::bits)
77            .try_fold(0, |bits, next| Some(next? | bits))
78    }
79
80    /// Returns `true` if no mods are contained.
81    ///
82    /// # Example
83    /// ```rust
84    /// use rosu_mods::{GameModIntermode, GameModsIntermode};
85    ///
86    /// let mut mods = GameModsIntermode::new();
87    /// assert!(mods.is_empty());
88    ///
89    /// mods.insert(GameModIntermode::Hidden);
90    /// assert!(!mods.is_empty());
91    /// ```
92    pub fn is_empty(&self) -> bool {
93        self.inner.is_empty()
94    }
95
96    /// Returns the amount of contained mods.
97    ///
98    /// # Example
99    /// ```rust
100    /// use rosu_mods::{mods, GameModIntermode, GameModsIntermode};
101    ///
102    /// let hdhrdt = mods!(HD HR DT);
103    /// assert_eq!(hdhrdt.len(), 3);
104    ///
105    /// let mut nm = GameModsIntermode::new();
106    /// assert_eq!(nm.len(), 0);
107    /// assert_eq!(nm.to_string(), "NM");
108    /// ```
109    pub fn len(&self) -> usize {
110        self.inner.len()
111    }
112
113    /// Add a [`GameModIntermode`]
114    ///
115    /// # Example
116    /// ```rust
117    /// use rosu_mods::{GameModIntermode, GameModsIntermode};
118    ///
119    /// let mut mods = GameModsIntermode::new();
120    /// assert_eq!(mods.to_string(), "NM");
121    ///
122    /// mods.insert(GameModIntermode::Traceable);
123    /// assert_eq!(mods.to_string(), "TC");
124    ///
125    /// mods.insert(GameModIntermode::HardRock);
126    /// assert_eq!(mods.to_string(), "HRTC");
127    /// ```
128    pub fn insert(&mut self, gamemod: GameModIntermode) {
129        self.inner.insert(gamemod);
130    }
131
132    /// Check whether a given mod is contained.
133    ///
134    /// # Example
135    /// ```rust
136    /// use rosu_mods::{mods, GameModIntermode};
137    ///
138    /// let hd = mods!(HD);
139    /// assert!(hd.contains(GameModIntermode::Hidden));
140    /// assert!(!hd.contains(GameModIntermode::HardRock));
141    /// ```
142    pub fn contains<M>(&self, gamemod: M) -> bool
143    where
144        GameModIntermode: From<M>,
145    {
146        self.inner.contains(&GameModIntermode::from(gamemod))
147    }
148
149    /// Check whether a given [`Acronym`] is contained.
150    ///
151    /// # Example
152    /// ```rust
153    /// use rosu_mods::{mods, Acronym};
154    ///
155    /// let nc = mods!(NC);
156    /// assert!(nc.contains_acronym("NC".parse::<Acronym>().unwrap()));
157    /// assert!(!nc.contains_acronym("DT".parse::<Acronym>().unwrap()));
158    /// ```
159    pub fn contains_acronym(&self, acronym: Acronym) -> bool {
160        self.inner
161            .iter()
162            .any(|gamemod| gamemod.acronym() == acronym)
163    }
164
165    /// Remove a gamemod and return whether it was contained.
166    ///
167    /// # Example
168    /// ```
169    /// use rosu_mods::{mods, GameModIntermode, GameModsIntermode};
170    ///
171    /// let mut mods: GameModsIntermode = mods!(HD HR);
172    ///
173    /// assert!(mods.remove(GameModIntermode::Hidden));
174    /// assert_eq!(mods.to_string(), "HR");
175    /// assert!(!mods.remove(GameModIntermode::DoubleTime));
176    /// ```
177    pub fn remove<M>(&mut self, gamemod: M) -> bool
178    where
179        GameModIntermode: From<M>,
180    {
181        self.inner.remove(&GameModIntermode::from(gamemod))
182    }
183
184    /// Remove all mods contained in the iterator.
185    ///
186    /// # Example
187    /// ```
188    /// use rosu_mods::{mods, GameModIntermode, GameModsIntermode};
189    ///
190    /// let mut mods: GameModsIntermode = mods!(HD HR WG DT BR);
191    ///
192    /// mods.remove_all([GameModIntermode::Hidden, GameModIntermode::Easy]);
193    /// assert_eq!(mods.to_string(), "DTHRBRWG");
194    ///
195    /// mods.remove_all(mods!(NF WG));
196    /// assert_eq!(mods.to_string(), "DTHRBR")
197    /// ```
198    pub fn remove_all<I, M>(&mut self, mods: I)
199    where
200        I: IntoIterator<Item = M>,
201        GameModIntermode: From<M>,
202    {
203        for gamemod in mods {
204            self.remove(gamemod);
205        }
206    }
207
208    /// Parse bitflags into [`GameModsIntermode`]
209    ///
210    /// # Example
211    /// ```rust
212    /// use rosu_mods::{mods, GameModsIntermode};
213    ///
214    /// let bits = 8 + 64 + 512 + 1024;
215    /// assert_eq!(GameModsIntermode::from_bits(bits), mods!(FL HD NC))
216    /// ```
217    pub fn from_bits(mut bits: u32) -> Self {
218        struct BitIterator(u32);
219
220        impl Iterator for BitIterator {
221            type Item = bool;
222
223            fn next(&mut self) -> Option<Self::Item> {
224                if self.0 == 0 {
225                    None
226                } else {
227                    let bit = self.0 & 0b1;
228                    self.0 >>= 1;
229
230                    Some(bit == 1)
231                }
232            }
233
234            fn size_hint(&self) -> (usize, Option<usize>) {
235                (usize::from(self.0 > 0), None)
236            }
237        }
238
239        // Special handling for NC and PF since they require two bits
240        bits &= if (bits & NightcoreOsu::bits()) == NightcoreOsu::bits() {
241            !DoubleTimeOsu::bits()
242        } else {
243            !(1 << 9)
244        };
245
246        bits &= if (bits & PerfectOsu::bits()) == PerfectOsu::bits() {
247            !SuddenDeathOsu::bits()
248        } else {
249            !(1 << 14)
250        };
251
252        #[allow(clippy::items_after_statements)]
253        const BITFLAG_MODS: [GameModIntermode; 31] = [
254            GameModIntermode::NoFail,
255            GameModIntermode::Easy,
256            GameModIntermode::TouchDevice,
257            GameModIntermode::Hidden,
258            GameModIntermode::HardRock,
259            GameModIntermode::SuddenDeath,
260            GameModIntermode::DoubleTime,
261            GameModIntermode::Relax,
262            GameModIntermode::HalfTime,
263            GameModIntermode::Nightcore,
264            GameModIntermode::Flashlight,
265            GameModIntermode::Autoplay,
266            GameModIntermode::SpunOut,
267            GameModIntermode::Autopilot,
268            GameModIntermode::Perfect,
269            GameModIntermode::FourKeys,
270            GameModIntermode::FiveKeys,
271            GameModIntermode::SixKeys,
272            GameModIntermode::SevenKeys,
273            GameModIntermode::EightKeys,
274            GameModIntermode::FadeIn,
275            GameModIntermode::Random,
276            GameModIntermode::Cinema,
277            GameModIntermode::TargetPractice,
278            GameModIntermode::NineKeys,
279            GameModIntermode::DualStages,
280            GameModIntermode::OneKey,
281            GameModIntermode::ThreeKeys,
282            GameModIntermode::TwoKeys,
283            GameModIntermode::ScoreV2,
284            GameModIntermode::Mirror,
285        ];
286
287        let inner = BitIterator(bits)
288            .zip(BITFLAG_MODS)
289            .filter_map(|(is_set, gamemod)| is_set.then_some(gamemod))
290            .collect();
291
292        Self { inner }
293    }
294
295    /// Try to parse a combination of mod acronyms into [`GameModsIntermode`].
296    ///
297    /// Returns `None` if an unknown acronym was encountered.
298    ///
299    /// # Example
300    /// ```rust
301    /// use rosu_mods::GameModsIntermode;
302    ///
303    /// let hdhrwu = GameModsIntermode::try_from_acronyms("HRWUHD").unwrap();
304    /// assert_eq!(hdhrwu.to_string(), "HDHRWU");
305    ///
306    /// assert!(GameModsIntermode::try_from_acronyms("QQQ").is_none());
307    /// ```
308    pub fn try_from_acronyms(s: &str) -> Option<Self> {
309        let uppercased = util::to_uppercase(s);
310
311        if uppercased == "NM" {
312            return Some(Self::new());
313        }
314
315        // We currently don't allow a gamemod to have an acronym of length 1
316        if s.len() == 1 {
317            return None;
318        }
319
320        let mut remaining = uppercased.as_ref();
321        let mut mods = BTreeSet::new();
322
323        while !remaining.is_empty() {
324            // Split off the first two characters and check if it's an acronym
325            let (candidate, rest) = util::split_prefix::<2>(remaining);
326
327            // SAFETY: `candidate` is guaranteed to be of length 2 and has been capitalized
328            let acronym = unsafe { Acronym::from_str_unchecked(candidate) };
329            let gamemod = GameModIntermode::from_acronym(acronym);
330
331            if !matches!(gamemod, GameModIntermode::Unknown(_)) && rest.len() != 1 {
332                mods.insert(gamemod);
333                remaining = rest;
334
335                continue;
336            }
337
338            // Repeat for the first three characters
339            let (candidate, rest) = util::split_prefix::<3>(remaining);
340
341            // SAFETY: `candidate` is guaranteed to be of length 3 and has been capitalized
342            let acronym = unsafe { Acronym::from_str_unchecked(candidate) };
343            let gamemod = GameModIntermode::from_acronym(acronym);
344
345            if matches!(gamemod, GameModIntermode::Unknown(_)) {
346                return None;
347            }
348
349            mods.insert(gamemod);
350            remaining = rest;
351        }
352
353        Some(Self { inner: mods })
354    }
355
356    /// Parse a combination of mod acronyms into [`GameModsIntermode`].
357    ///
358    /// # Example
359    /// ```rust
360    /// use rosu_mods::GameModsIntermode;
361    ///
362    /// let hdhrwu = GameModsIntermode::from_acronyms("HRWUHD");
363    /// assert_eq!(hdhrwu.len(), 3);
364    /// assert_eq!(hdhrwu.to_string(), "HDHRWU");
365    ///
366    /// let mut iter = GameModsIntermode::from_acronyms("QQhdQ").into_iter();
367    /// assert_eq!(iter.next().unwrap().to_string(), "HDQ"); // unknown mod
368    /// assert_eq!(iter.next().unwrap().to_string(), "QQ");  // unknown mod
369    /// assert!(iter.next().is_none());
370    /// ```
371    pub fn from_acronyms(s: &str) -> Self {
372        let uppercased = util::to_uppercase(s);
373
374        if uppercased == "NM" {
375            return Self::new();
376        }
377
378        let mut mods = BTreeSet::new();
379
380        // We currently don't allow a gamemod to have an acronym of length 1
381        let mut remaining = if s.len() == 1 {
382            mods.insert(GameModIntermode::Unknown(Default::default()));
383
384            ""
385        } else {
386            uppercased.as_ref()
387        };
388
389        while !remaining.is_empty() {
390            // Split off the first two characters and check if it's an acronym
391            let (candidate, rest) = util::split_prefix::<2>(remaining);
392
393            // SAFETY: `candidate` is guaranteed to be of length 2 and has been capitalized
394            let acronym = unsafe { Acronym::from_str_unchecked(candidate) };
395            let gamemod = GameModIntermode::from_acronym(acronym);
396
397            if !matches!(gamemod, GameModIntermode::Unknown(_)) && rest.len() != 1 {
398                mods.insert(gamemod);
399                remaining = rest;
400
401                continue;
402            }
403
404            // Repeat for the first three characters
405            let (candidate, three_letter_rest) = util::split_prefix::<3>(remaining);
406
407            // SAFETY: `candidate` is guaranteed to be of length 3 and has been capitalized
408            let acronym = unsafe { Acronym::from_str_unchecked(candidate) };
409            let three_letter_gamemod = GameModIntermode::from_acronym(acronym);
410
411            if !matches!(three_letter_gamemod, GameModIntermode::Unknown(_))
412                || three_letter_rest.is_empty()
413            {
414                mods.insert(three_letter_gamemod);
415                remaining = three_letter_rest;
416            } else {
417                mods.insert(gamemod);
418                remaining = rest;
419            }
420        }
421
422        Self { inner: mods }
423    }
424
425    /// Returns an iterator over all mods that appear in both [`GameModsIntermode`].
426    ///
427    /// # Example
428    /// ```rust
429    /// use rosu_mods::{mods, GameModIntermode};
430    ///
431    /// let hd = mods!(HD);
432    /// let hdhr = mods!(HD HR);
433    /// let mut intersection = hd.intersection(&hdhr);
434    /// assert_eq!(intersection.next(), Some(GameModIntermode::Hidden));
435    /// assert_eq!(intersection.next(), None);
436    /// ```
437    // https://github.com/rust-lang/rust/blob/c1d3610ac1ddd1cd605479274047fd0a3f37d220/library/alloc/src/collections/btree/set.rs#L517
438    pub fn intersection<'m>(
439        &'m self,
440        other: &'m GameModsIntermode,
441    ) -> GameModsIntermodeIntersection<'m> {
442        let (self_min, self_max) =
443            if let (Some(self_min), Some(self_max)) = (self.inner.first(), self.inner.last()) {
444                (*self_min, *self_max)
445            } else {
446                return GameModsIntermodeIntersection {
447                    inner: IntersectionInner::Answer(None),
448                };
449            };
450
451        let (other_min, other_max) =
452            if let (Some(other_min), Some(other_max)) = (other.inner.first(), other.inner.last()) {
453                (*other_min, *other_max)
454            } else {
455                return GameModsIntermodeIntersection {
456                    inner: IntersectionInner::Answer(None),
457                };
458            };
459
460        GameModsIntermodeIntersection {
461            inner: match (self_min.cmp(&other_max), self_max.cmp(&other_min)) {
462                (Ordering::Greater, _) | (_, Ordering::Less) => IntersectionInner::Answer(None),
463                (Ordering::Equal, _) => IntersectionInner::Answer(Some(self_min)),
464                (_, Ordering::Equal) => IntersectionInner::Answer(Some(self_max)),
465                _ => IntersectionInner::new_stitch(self.inner.iter(), other.inner.iter()),
466            },
467        }
468    }
469
470    /// Check whether the two [`GameMods`] have any common mods.
471    ///
472    /// # Example
473    /// ```rust
474    /// use rosu_mods::mods;
475    ///
476    /// let hd = mods!(HD);
477    /// assert!(!hd.intersects(&mods!(HR)));
478    /// assert!(hd.intersects(&mods!(HD HR)));
479    /// ```
480    pub fn intersects(&self, other: &Self) -> bool {
481        self.intersection(other).next().is_some()
482    }
483
484    /// The legacy clock rate of the [`GameModsIntermode`].
485    ///
486    /// Looks for the first occurrence of DT, NC, HT, or DC
487    /// and returns `1.5`, `0.75`, or `1.0` accordingly.
488    ///
489    /// # Example
490    /// ```rust
491    /// use rosu_mods::{mods, GameModIntermode};
492    ///
493    /// let hd = mods!(HD);
494    /// assert_eq!(hd.legacy_clock_rate(), 1.0);
495    ///
496    /// let mut hddt = hd;
497    /// hddt.insert(GameModIntermode::DoubleTime);
498    /// assert_eq!(hddt.legacy_clock_rate(), 1.5);
499    /// ```
500    pub fn legacy_clock_rate(&self) -> f64 {
501        self.iter()
502            .find_map(|gamemod| match gamemod {
503                GameModIntermode::DoubleTime | GameModIntermode::Nightcore => Some(1.5),
504                GameModIntermode::HalfTime | GameModIntermode::Daycore => Some(0.75),
505                _ => None,
506            })
507            .unwrap_or(1.0)
508    }
509
510    /// Returns an iterator over all contained mods.
511    ///
512    /// Note that the iterator will immediately yield `None` in case of "`NoMod`".
513    pub fn iter(&self) -> GameModsIntermodeIter<'_> {
514        GameModsIntermodeIter::new(self.inner.iter().copied())
515    }
516
517    /// Tries to turn a [`GameModsIntermode`] into a [`GameMods`].
518    ///
519    /// Returns `None` if any contained [`GameModIntermode`] is unknown for the
520    /// given [`GameMode`].
521    ///
522    /// # Example
523    /// ```rust
524    /// use rosu_mods::{mods, GameMods, GameMode};
525    ///
526    /// let dtfi: GameMods = mods!(DT FI).try_with_mode(GameMode::Mania).unwrap();
527    ///
528    /// // The FadeIn mod doesn't exist in Taiko
529    /// assert!(mods!(DT FI).try_with_mode(GameMode::Taiko).is_none());
530    /// ```
531    pub fn try_with_mode(&self, mode: GameMode) -> Option<GameMods> {
532        self.inner
533            .iter()
534            .map(|gamemod| GameMod::new(gamemod.acronym().as_str(), mode))
535            .try_fold(GameMods::default(), |mut mods, next| {
536                if matches!(
537                    next,
538                    GameMod::UnknownOsu(_)
539                        | GameMod::UnknownTaiko(_)
540                        | GameMod::UnknownCatch(_)
541                        | GameMod::UnknownMania(_)
542                ) {
543                    None
544                } else {
545                    mods.insert(next);
546
547                    Some(mods)
548                }
549            })
550    }
551
552    /// Turn a [`GameModsIntermode`] into a [`GameMods`].
553    ///
554    /// Any contained [`GameModIntermode`] that's unknown for the given
555    /// [`GameMode`] will be replaced with `GameModIntermode::Unknown`.
556    ///
557    /// # Example
558    /// ```rust
559    /// use rosu_mods::{mods, GameMods, GameMode};
560    ///
561    /// let dtfi: GameMods = mods!(DT FI).with_mode(GameMode::Mania);
562    ///
563    /// // The FadeIn mod doesn't exist in Taiko
564    /// let dt_unknown: GameMods = mods!(DT FI).with_mode(GameMode::Taiko);
565    /// assert_eq!(dt_unknown.to_string(), "DTFI");
566    /// ```
567    pub fn with_mode(&self, mode: GameMode) -> GameMods {
568        self.inner
569            .iter()
570            .map(|gamemod| GameMod::new(gamemod.acronym().as_str(), mode))
571            .collect()
572    }
573
574    /// Turns [`GameModsIntermode`] into [`GameModsLegacy`].
575    pub fn as_legacy(&self) -> GameModsLegacy {
576        GameModsLegacy::from_bits(self.bits())
577    }
578
579    /// Attempts to turns [`GameModsIntermode`] into [`GameModsLegacy`].
580    ///
581    /// Returns `None` if any contained [`GameModIntermode`] does not have a
582    /// bit value.
583    pub fn try_as_legacy(&self) -> Option<GameModsLegacy> {
584        self.checked_bits().map(GameModsLegacy::from_bits)
585    }
586}
587
588impl Debug for GameModsIntermode {
589    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
590        f.debug_list().entries(self.inner.iter()).finish()
591    }
592}
593
594impl Display for GameModsIntermode {
595    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
596        if self.is_empty() {
597            f.write_str("NM")
598        } else {
599            for gamemod in self.iter() {
600                f.write_str(gamemod.acronym().as_str())?;
601            }
602
603            Ok(())
604        }
605    }
606}
607
608impl IntoIterator for GameModsIntermode {
609    type Item = GameModIntermode;
610    type IntoIter = IntoGameModsIntermodeIter;
611
612    /// Turns [`GameModsIntermode`] into an iterator over all contained mods.
613    ///
614    /// Note that the iterator will immediately yield `None` in case of "`NoMod`".
615    fn into_iter(self) -> Self::IntoIter {
616        IntoGameModsIntermodeIter::new(self.inner.into_iter())
617    }
618}
619
620impl<'a> IntoIterator for &'a GameModsIntermode {
621    type Item = <GameModsIntermodeIter<'a> as Iterator>::Item;
622    type IntoIter = GameModsIntermodeIter<'a>;
623
624    fn into_iter(self) -> Self::IntoIter {
625        self.iter()
626    }
627}
628
629impl<M> FromIterator<M> for GameModsIntermode
630where
631    GameModIntermode: From<M>,
632{
633    fn from_iter<T: IntoIterator<Item = M>>(iter: T) -> Self {
634        Self {
635            inner: iter.into_iter().map(GameModIntermode::from).collect(),
636        }
637    }
638}
639
640impl<M> Extend<M> for GameModsIntermode
641where
642    GameModIntermode: From<M>,
643{
644    fn extend<T: IntoIterator<Item = M>>(&mut self, iter: T) {
645        self.inner
646            .extend(iter.into_iter().map(GameModIntermode::from));
647    }
648}
649
650impl BitOr<GameModIntermode> for GameModsIntermode {
651    type Output = Self;
652
653    /// Adds a [`GameModIntermode`] to the [`GameModsIntermode`].
654    fn bitor(mut self, rhs: GameModIntermode) -> Self::Output {
655        self |= rhs;
656
657        self
658    }
659}
660
661impl BitOrAssign<GameModIntermode> for GameModsIntermode {
662    /// Adds a [`GameModIntermode`] to the [`GameModsIntermode`].
663    fn bitor_assign(&mut self, rhs: GameModIntermode) {
664        self.insert(rhs);
665    }
666}
667
668impl Sub<GameModIntermode> for GameModsIntermode {
669    type Output = Self;
670
671    /// Removes a [`GameModIntermode`] from the [`GameModsIntermode`]
672    fn sub(mut self, rhs: GameModIntermode) -> Self::Output {
673        self -= rhs;
674
675        self
676    }
677}
678
679impl SubAssign<GameModIntermode> for GameModsIntermode {
680    /// Removes a [`GameModIntermode`] from the [`GameModsIntermode`]
681    fn sub_assign(&mut self, rhs: GameModIntermode) {
682        self.remove(rhs);
683    }
684}
685
686impl From<GameMods> for GameModsIntermode {
687    fn from(mods: GameMods) -> Self {
688        Self {
689            inner: mods.inner.values().map(GameMod::intermode).collect(),
690        }
691    }
692}
693
694impl From<GameModsLegacy> for GameModsIntermode {
695    fn from(mods: GameModsLegacy) -> Self {
696        mods.to_intermode()
697    }
698}
699
700impl From<GameModIntermode> for GameModsIntermode {
701    fn from(gamemod: GameModIntermode) -> Self {
702        let mut mods = Self::new();
703        mods.insert(gamemod);
704
705        mods
706    }
707}
708
709impl FromStr for GameModsIntermode {
710    type Err = Infallible;
711
712    fn from_str(s: &str) -> Result<Self, Self::Err> {
713        Ok(Self::from_acronyms(s))
714    }
715}
716
717#[cfg(feature = "serde")]
718#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "serde")))]
719const _: () = {
720    use serde::{
721        de::{Deserialize, Deserializer, Error as DeError, SeqAccess, Visitor},
722        ser::{Serialize, Serializer},
723    };
724
725    use crate::serde::{GameModRaw, GameModRawSeed, MaybeOwnedStr, BITFLAGS_U32};
726
727    impl<'de> Deserialize<'de> for GameModsIntermode {
728        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
729            struct GameModsIntermodeVisitor;
730
731            impl<'de> Visitor<'de> for GameModsIntermodeVisitor {
732                type Value = GameModsIntermode;
733
734                fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
735                    f.write_str("integer bitflags, mod acronyms, or a sequence of mods")
736                }
737
738                fn visit_i64<E: DeError>(self, v: i64) -> Result<Self::Value, E> {
739                    let bits = u32::try_from(v).map_err(|_| DeError::custom(BITFLAGS_U32))?;
740
741                    self.visit_u32(bits)
742                }
743
744                fn visit_u32<E: DeError>(self, v: u32) -> Result<Self::Value, E> {
745                    Ok(GameModsIntermode::from_bits(v))
746                }
747
748                fn visit_u64<E: DeError>(self, v: u64) -> Result<Self::Value, E> {
749                    let bits = u32::try_from(v).map_err(|_| DeError::custom(BITFLAGS_U32))?;
750
751                    self.visit_u32(bits)
752                }
753
754                fn visit_str<E: DeError>(self, v: &str) -> Result<Self::Value, E> {
755                    Ok(GameModsIntermode::from_acronyms(v))
756                }
757
758                fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
759                    let mut inner = BTreeSet::new();
760                    let seed = GameModRawSeed {
761                        deny_unknown_fields: true,
762                    };
763
764                    while let Some(raw) = seq.next_element_seed(seed)? {
765                        fn try_acronym_to_gamemod<E: DeError>(
766                            acronym: &MaybeOwnedStr<'_>,
767                        ) -> Result<GameModIntermode, E> {
768                            acronym
769                                .as_str()
770                                .parse()
771                                .map(GameModIntermode::from_acronym)
772                                .map_err(DeError::custom)
773                        }
774
775                        let gamemod = match raw {
776                            GameModRaw::Bits(bits) => GameModIntermode::try_from_bits(bits)
777                                .ok_or_else(|| DeError::custom("invalid bitflags"))?,
778                            GameModRaw::Acronym(acronym) => try_acronym_to_gamemod(&acronym)?,
779                            GameModRaw::Full { acronym, .. } => try_acronym_to_gamemod(&acronym)?,
780                        };
781
782                        inner.insert(gamemod);
783                    }
784
785                    Ok(GameModsIntermode { inner })
786                }
787            }
788
789            d.deserialize_any(GameModsIntermodeVisitor)
790        }
791    }
792
793    impl Serialize for GameModsIntermode {
794        fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
795            self.inner.serialize(s)
796        }
797    }
798};
799
800#[cfg(feature = "rkyv")]
801#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "rkyv")))]
802const _: () = {
803    use rkyv::{
804        rancor::Fallible,
805        ser::{Allocator, Writer},
806        vec::{ArchivedVec, VecResolver},
807        Archive, Archived, Deserialize, Place, Serialize,
808    };
809
810    impl Archive for GameModsIntermode {
811        type Archived = Archived<Vec<GameModIntermode>>;
812        type Resolver = VecResolver;
813
814        fn resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>) {
815            ArchivedVec::resolve_from_len(self.inner.len(), resolver, out);
816        }
817    }
818
819    impl<S: Fallible + Allocator + Writer + ?Sized> Serialize<S> for GameModsIntermode {
820        fn serialize(&self, s: &mut S) -> Result<Self::Resolver, S::Error> {
821            ArchivedVec::serialize_from_iter::<GameModIntermode, _, _>(self.inner.iter(), s)
822        }
823    }
824
825    impl<D: Fallible + ?Sized> Deserialize<GameModsIntermode, D>
826        for ArchivedVec<Archived<GameModIntermode>>
827    {
828        fn deserialize(&self, _: &mut D) -> Result<GameModsIntermode, D::Error> {
829            Ok(self.iter().copied().collect())
830        }
831    }
832};
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837
838    #[test]
839    fn push() {
840        let mut mods = GameModsIntermode::default();
841        mods.insert(GameModIntermode::HardRock);
842        mods.insert(GameModIntermode::Wiggle);
843
844        assert_eq!(mods.len(), 2);
845        assert_eq!(mods.to_string(), "HRWG");
846    }
847
848    #[test]
849    fn from_bits_nomod() {
850        assert!(GameModsIntermode::from_bits(0).is_empty());
851    }
852
853    #[test]
854    fn from_bits_valid() {
855        let mut expected = GameModsIntermode::default();
856        expected.insert(GameModIntermode::Nightcore);
857        expected.insert(GameModIntermode::Hidden);
858
859        assert_eq!(GameModsIntermode::from_bits(8 + 64 + 512), expected);
860    }
861
862    #[test]
863    fn from_bits_invalid_nightcore() {
864        assert_eq!(GameModsIntermode::from_bits(512), GameModsIntermode::new());
865    }
866
867    #[test]
868    fn from_str_nonempty() {
869        let mods: GameModsIntermode = "TCWGFLWU".parse().unwrap();
870
871        let mut expected = GameModsIntermode::default();
872        expected.insert(GameModIntermode::Flashlight);
873        expected.insert(GameModIntermode::Traceable);
874        expected.insert(GameModIntermode::Wiggle);
875        expected.insert(GameModIntermode::WindUp);
876
877        assert_eq!(mods, expected);
878    }
879
880    #[test]
881    fn from_str_unknown() {
882        let mut iter = "YYQQQ".parse::<GameModsIntermode>().unwrap().into_iter();
883
884        // Since acronyms of length 1 are not valid, it picks the last three
885        // characters.
886        // Also, "QQQ" comes before "YY" alphabetically so it'll be the first mod.
887        assert_eq!(iter.next().unwrap().to_string(), "QQQ");
888        assert_eq!(iter.next().unwrap().to_string(), "YY");
889        assert!(iter.next().is_none());
890    }
891
892    #[test]
893    fn contains() {
894        let mut mods = GameModsIntermode::default();
895        mods.insert(GameModIntermode::Hidden);
896        mods.insert(GameModIntermode::HardRock);
897        mods.insert(GameModIntermode::Nightcore);
898
899        assert!(mods.contains(GameModIntermode::Nightcore));
900        assert!(mods.contains(GameModIntermode::Hidden));
901        assert!(!mods.contains(GameModIntermode::DoubleTime));
902    }
903
904    #[test]
905    fn checked_bits() {
906        let mut mods = GameModsIntermode::default();
907        mods.insert(GameModIntermode::Hidden);
908        mods.insert(GameModIntermode::Traceable);
909        mods.insert(GameModIntermode::DoubleTime);
910
911        assert_eq!(mods.checked_bits(), None);
912    }
913
914    #[test]
915    fn unchecked_bits() {
916        let mut mods = GameModsIntermode::default();
917        mods.insert(GameModIntermode::Traceable);
918        mods.insert(GameModIntermode::DoubleTime);
919        mods.insert(GameModIntermode::Hidden);
920
921        assert_eq!(mods.bits(), 72);
922    }
923
924    #[test]
925    fn intersection() {
926        let mut a = GameModsIntermode::default();
927        a.insert(GameModIntermode::Hidden);
928        a.insert(GameModIntermode::WindUp);
929        a.insert(GameModIntermode::HardRock);
930
931        let mut b = GameModsIntermode::default();
932        b.insert(GameModIntermode::WindUp);
933        b.insert(GameModIntermode::Classic);
934        b.insert(GameModIntermode::HardRock);
935
936        let mut intersection = a.intersection(&b);
937        assert_eq!(intersection.next(), Some(GameModIntermode::HardRock));
938        assert_eq!(intersection.next(), Some(GameModIntermode::WindUp));
939        assert_eq!(intersection.next(), None);
940    }
941
942    #[cfg(feature = "serde")]
943    mod serde {
944        use super::*;
945
946        #[test]
947        fn deser_str() {
948            let json = r#""HDHRWG""#;
949            let mods = serde_json::from_str::<GameModsIntermode>(json).unwrap();
950
951            let mut expected = GameModsIntermode::default();
952            expected.insert(GameModIntermode::Hidden);
953            expected.insert(GameModIntermode::HardRock);
954            expected.insert(GameModIntermode::Wiggle);
955
956            assert_eq!(mods, expected);
957        }
958
959        #[test]
960        fn deser_bits() {
961            let json = "1096";
962            let mods = serde_json::from_str::<GameModsIntermode>(json).unwrap();
963
964            let mut expected = GameModsIntermode::default();
965            expected.insert(GameModIntermode::Hidden);
966            expected.insert(GameModIntermode::DoubleTime);
967            expected.insert(GameModIntermode::Flashlight);
968
969            assert_eq!(mods, expected);
970        }
971
972        #[test]
973        fn deser_seq() {
974            let json = r#"["WU", "BL", 2, { "acronym": "NS", "settings": { "any": true } }]"#;
975            let mods = serde_json::from_str::<GameModsIntermode>(json).unwrap();
976
977            let mut expected = GameModsIntermode::default();
978            expected.insert(GameModIntermode::Blinds);
979            expected.insert(GameModIntermode::Easy);
980            expected.insert(GameModIntermode::WindUp);
981            expected.insert(GameModIntermode::NoScope);
982
983            assert_eq!(mods, expected);
984        }
985    }
986}